返回 oh-my-ppt
page-merge-resource-rewriter.test.ts
根目录 / tests / unit / session / page-merge-resource-rewriter.test.ts
1 import fs from 'fs'
2 import os from 'os'
3 import path from 'path'
4 import { describe, expect, it } from 'vitest'
5 import {
6 collectMergedPageResourceKeys,
7 collectUnsafeMergedPageResourceReferences,
8 extractMergePageFontProfile,
9 isMergePathInside,
10 resolveMergeFileInside,
11 rewriteMergedPageHtml
12 } from '../../../src/main/session/page-merge-rewriter'
13 import { validatePersistedPageHtml } from '../../../src/main/presentation/html/html-utils'
14
15 const targetFontHtml = `<html><head>
16 <style data-ppt-fonts="google">@font-face{font-family:"Target Title";src:url("./assets/fonts/google-fonts/Target-Title/title.woff2") format("woff2")}</style>
17 <style data-ppt-fonts="google">@font-face{font-family:"Target Body";src:url("./assets/fonts/google-fonts/Target-Body/body.woff2") format("woff2")}</style>
18 <style data-ppt-fonts="1">:root{--ppt-title-font:"Target Title";--ppt-body-font:"Target Body"}</style>
19 </head><body></body></html>`
20
21 const targetFontProfile = extractMergePageFontProfile(targetFontHtml)!
22
23 describe('page merge resource rewriter', () => {
24 it('rewrites page identity and local resource attributes', () => {
25 const html = `<!doctype html>
26 <html>
27 <head>
28 <style>.hero{background-image:url('./images/bg.png?x=1')}</style>
29 </head>
30 <body data-page-id="page-old">
31 <div data-page-id="page-old" style="background:url(./images/pattern.svg#mask)">
32 <img src="./images/a.png" srcset="./images/a.png 1x, ./images/a@2x.png 2x">
33 <img src="./images/page-old.png">
34 <video src="./videos/demo.mp4" poster="./images/poster.jpg"></video>
35 <svg>
36 <image href="./images/vector.svg" />
37 <image xlink:href="./images/vector-legacy.svg" />
38 </svg>
39 </div>
40 <script>window.pageId = 'page-old'</script>
41 </body>
42 </html>`
43 const resourcePathMap = new Map([
44 ['images/bg.png', './assets/merged-pages/batch/page-new/images/bg.png'],
45 ['images/pattern.svg', './assets/merged-pages/batch/page-new/images/pattern.svg'],
46 ['images/a.png', './assets/merged-pages/batch/page-new/images/a.png'],
47 ['images/a@2x.png', './assets/merged-pages/batch/page-new/images/a@2x.png'],
48 ['images/page-old.png', './assets/merged-pages/batch/page-new/images/page-old.png'],
49 ['videos/demo.mp4', './assets/merged-pages/batch/page-new/videos/demo.mp4'],
50 ['images/poster.jpg', './assets/merged-pages/batch/page-new/images/poster.jpg'],
51 ['images/vector.svg', './assets/merged-pages/batch/page-new/images/vector.svg'],
52 ['images/vector-legacy.svg', './assets/merged-pages/batch/page-new/images/vector-legacy.svg']
53 ])
54
55 const rewritten = rewriteMergedPageHtml({
56 html,
57 oldPageId: 'page-old',
58 nextPageId: 'page-new',
59 resourcePathMap,
60 targetFontProfile
61 })
62
63 expect(rewritten).toContain('data-page-id="page-new"')
64 expect(rewritten).toContain("window.pageId = 'page-new'")
65 expect(rewritten).toContain('./assets/merged-pages/batch/page-new/images/a.png')
66 expect(rewritten).toContain('./assets/merged-pages/batch/page-new/images/a@2x.png 2x')
67 expect(rewritten).toContain('./assets/merged-pages/batch/page-new/images/page-old.png')
68 expect(rewritten).toContain('./assets/merged-pages/batch/page-new/videos/demo.mp4')
69 expect(rewritten).toContain('./assets/merged-pages/batch/page-new/images/bg.png?x=1')
70 expect(rewritten).toContain('./assets/merged-pages/batch/page-new/images/pattern.svg#mask')
71 expect(rewritten).toContain('./assets/merged-pages/batch/page-new/images/vector.svg')
72 expect(rewritten).toContain('./assets/merged-pages/batch/page-new/images/vector-legacy.svg')
73 })
74
75 it('collects local resources and ignores external or unsafe references', () => {
76 const html = `
77 <img src="./images/a.png">
78 <img src="https://example.com/remote.png">
79 <img src="data:image/png;base64,abc">
80 <img src="../outside.png">
81 <video src="./videos/demo.mp4#t=1"></video>
82 <script src="./assets/ppt-runtime.js"></script>
83 <div style="background:url('./images/bg.png')"></div>
84 `
85
86 expect(collectMergedPageResourceKeys(html)).toEqual([
87 'assets/ppt-runtime.js',
88 'images/a.png',
89 'images/bg.png',
90 'videos/demo.mp4'
91 ])
92 })
93
94 it('reports unsafe non-font resource references', () => {
95 expect(
96 collectUnsafeMergedPageResourceReferences(`
97 <img src="../outside.png">
98 <div style="background:url('%2e%2e/encoded.png')"></div>
99 <style>@font-face{src:url('../fonts/font.woff2')}</style>
100 `)
101 ).toEqual(['%2e%2e/encoded.png', '../outside.png'])
102 })
103
104 it('keeps resources that are not present in the rewrite map', () => {
105 const html = `
106 <body data-page-id="page-a">
107 <script src="./assets/ppt-runtime.js"></script>
108 <img src="https://example.com/image.png">
109 </body>
110 `
111 const rewritten = rewriteMergedPageHtml({
112 html,
113 oldPageId: 'page-a',
114 nextPageId: 'page-b',
115 resourcePathMap: new Map(),
116 targetFontProfile
117 })
118
119 expect(rewritten).toContain('./assets/ppt-runtime.js')
120 expect(rewritten).toContain('https://example.com/image.png')
121 })
122
123 it('rejects paths that escape the source project root', () => {
124 expect(isMergePathInside('/sessions/source/images/a.png', '/sessions/source')).toBe(true)
125 expect(isMergePathInside('/sessions/source', '/sessions/source')).toBe(true)
126 expect(isMergePathInside('/sessions/source-other/a.png', '/sessions/source')).toBe(false)
127 expect(isMergePathInside('/sessions/outside.png', '/sessions/source')).toBe(false)
128 })
129
130 it('rejects symlinks that resolve outside the source project root', async () => {
131 const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'page-merge-path-'))
132 const sourceRoot = path.join(root, 'source')
133 const outsidePath = path.join(root, 'outside.png')
134 await fs.promises.mkdir(sourceRoot, { recursive: true })
135 await fs.promises.writeFile(outsidePath, 'outside')
136 const linkedPath = path.join(sourceRoot, 'linked.png')
137 await fs.promises.symlink(outsidePath, linkedPath)
138
139 try {
140 await expect(resolveMergeFileInside(linkedPath, sourceRoot)).resolves.toBeNull()
141 } finally {
142 await fs.promises.rm(root, { recursive: true, force: true })
143 }
144 })
145
146 it('replaces source font injection with the target template fonts', () => {
147 const html = `<html><head>
148 <style data-ppt-fonts="google">@font-face{font-family:"Source Title";src:url("./assets/fonts/google-fonts/Source-Title/title.woff2") format("woff2")}</style>
149 <style data-ppt-fonts="google">@font-face{font-family:"Source Body";src:url("./assets/fonts/google-fonts/Source-Body/body.woff2") format("woff2")}</style>
150 <style data-ppt-fonts="1">:root{--ppt-title-font:"Source Title";--ppt-body-font:"Source Body"}</style>
151 <style>@font-face{font-family:"Legacy Font";src:url("./legacy.woff2")}.title{font-family:"Source Title"}.body{font-family:Source Body}.legacy{font-family:"Legacy Font"}</style>
152 </head><body data-page-id="page-source"><p style="font-family:'Source Body'">Text</p></body></html>`
153
154 const rewritten = rewriteMergedPageHtml({
155 html,
156 oldPageId: 'page-source',
157 nextPageId: 'page-target',
158 resourcePathMap: new Map(),
159 targetFontProfile
160 })
161
162 expect(rewritten).not.toContain('Source Title')
163 expect(rewritten).not.toContain('Source Body')
164 expect(rewritten).not.toContain('Source-Title')
165 expect(rewritten).not.toContain('Legacy Font')
166 expect(rewritten).not.toContain('@font-face{font-family:"Legacy Font"')
167 expect(rewritten).toContain('--ppt-title-font:"Target Title"')
168 expect(rewritten).toContain('--ppt-body-font:"Target Body"')
169 expect(rewritten).toContain('./assets/fonts/google-fonts/Target-Title/title.woff2')
170 expect(rewritten).not.toContain('assets/merged-pages')
171 expect(
172 validatePersistedPageHtml(rewritten, 'page-target').errors.filter((error) =>
173 error.includes('字体')
174 )
175 ).toEqual([])
176 })
177 })
178
178 lines TYPESCRIPT