forked from nexu-io/open-design
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexport-title-replacement-patterns.test.ts
More file actions
125 lines (110 loc) · 4.4 KB
/
Copy pathexport-title-replacement-patterns.test.ts
File metadata and controls
125 lines (110 loc) · 4.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
// Regression (#6795): artifact titles containing String.prototype.replace
// replacement patterns (`$$`, `$&`, `$``, `$'`) must land in exported PDF /
// image documents verbatim (HTML-escaped only). A string replacement in
// `injectTitle` would expand them and corrupt the rendered document.
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
const rendererState = vi.hoisted(() => ({ loadedUrls: [] as string[], saveDir: '' }));
vi.mock('electron', () => {
const image = {
getSize: () => ({ height: 1, width: 1 }),
toBitmap: () => Buffer.alloc(4),
toJPEG: () => Buffer.from('jpeg'),
toPNG: () => Buffer.from('png'),
};
class BrowserWindow {
readonly webContents = {
capturePage: async () => image,
debugger: {
attach: () => {
throw new Error('debugger unavailable in title-pattern test');
},
detach: () => undefined,
sendCommand: async () => undefined,
},
executeJavaScript: async (source: string): Promise<unknown> => {
if (source.includes("document.querySelectorAll('.slide")) return 0;
if (source.includes('document.documentElement.scrollHeight')) return 1;
if (source === 'window.devicePixelRatio || 1') return 1;
return true;
},
on: () => undefined,
printToPDF: async () => Buffer.from('pdf'),
setWindowOpenHandler: () => undefined,
};
async loadURL(url: string): Promise<void> {
rendererState.loadedUrls.push(url);
}
destroy(): void {}
getContentSize(): [number, number] { return [1440, 900]; }
isDestroyed(): boolean { return false; }
setContentSize(): void {}
setOpacity(): void {}
showInactive(): void {}
}
return {
BrowserWindow,
dialog: { showSaveDialog: async () => ({ canceled: false, filePath: join(rendererState.saveDir, 'out.pdf') }) },
nativeImage: { createFromBitmap: () => image },
};
});
import { exportArtifact } from '../../src/main/artifact-export.js';
import { exportPdfFromHtml } from '../../src/main/pdf-export.js';
const SOURCE_DOC = '<html><head><title>Old</title></head><body><p>BODY MARKER</p></body></html>';
const TITLES = ['Save $$$ This Quarter', "Rock $'n Roll Tour", 'Before $& After', 'Backtick $` Pattern'] as const;
beforeAll(async () => {
rendererState.saveDir = await mkdtemp(join(tmpdir(), 'od-title-export-'));
});
afterAll(async () => {
await rm(rendererState.saveDir, { force: true, recursive: true });
});
function lastLoadedDocument(): string {
const url = rendererState.loadedUrls[rendererState.loadedUrls.length - 1];
if (!url?.startsWith('data:text/html;charset=utf-8,')) throw new Error(`unexpected loaded URL: ${url}`);
return decodeURIComponent(url.slice('data:text/html;charset=utf-8,'.length));
}
/** The exact HTML-escaping the exporters apply before interpolation. */
function escapedTitle(title: string): string {
return title.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
describe('export title replacement-pattern safety (#6795)', () => {
it('keeps replacement-pattern sequences verbatim in PDF export titles', async () => {
for (const title of TITLES) {
rendererState.loadedUrls.length = 0;
const result = await exportPdfFromHtml({
baseHref: undefined,
deck: true,
defaultFilename: 'out.pdf',
html: SOURCE_DOC,
title,
});
expect(result.ok).toBe(true);
const doc = lastLoadedDocument();
expect(doc).toContain(`<title>${escapedTitle(title)}</title>`);
expect(doc).toContain('<p>BODY MARKER</p>');
}
}, 30_000);
it('keeps replacement-pattern sequences verbatim in image export titles', async () => {
for (const title of TITLES) {
rendererState.loadedUrls.length = 0;
const result = await exportArtifact({
baseHref: undefined,
deck: false,
format: 'image',
html: SOURCE_DOC,
imageFormat: 'png',
title,
} as const);
try {
expect(result.ok).toBe(true);
const doc = lastLoadedDocument();
expect(doc).toContain(`<title>${escapedTitle(title)}</title>`);
expect(doc).toContain('<p>BODY MARKER</p>');
} finally {
if (result.path) await rm(dirname(result.path), { force: true, recursive: true });
}
}
}, 30_000);
});