-
Notifications
You must be signed in to change notification settings - Fork 609
Expand file tree
/
Copy pathdoc-export.spec.ts
More file actions
420 lines (324 loc) · 14.1 KB
/
Copy pathdoc-export.spec.ts
File metadata and controls
420 lines (324 loc) · 14.1 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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
import path from 'path';
import { expect, test } from '@playwright/test';
import cs from 'convert-stream';
import JSZip from 'jszip';
import { PDFParse } from 'pdf-parse';
import {
TestLanguage,
clickInEditorMenu,
createDoc,
verifyDocName,
waitForLanguageSwitch,
} from './utils-common';
import { openSuggestionMenu, writeInEditor } from './utils-editor';
import { comparePDFWithAssetFolder, overrideDocContent } from './utils-export';
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test.describe('Doc Export', () => {
test('it check if all elements are visible', async ({
page,
browserName,
}) => {
await createDoc(page, 'doc-editor', browserName, 1);
await clickInEditorMenu(page, 'Download');
await expect(page.getByTestId('modal-export-title')).toBeVisible();
await expect(
page.getByText(
'Export your document to print or download in .docx, .odt, .pdf or .html(zip) format.',
),
).toBeVisible();
await expect(page.getByRole('combobox', { name: 'Format' })).toBeVisible();
await expect(
page.getByRole('button', {
name: 'Close the download modal',
}),
).toBeVisible();
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
});
/**
* We override the document content to ensure that the exported DOCX
* contains various elements for testing.
* We don't check the content of the DOCX here, just that the export works
* and the file is correctly named.
*/
test('it exports the doc to docx', async ({ page, browserName }) => {
const randomDoc = await overrideDocContent({ page, browserName });
await clickInEditorMenu(page, 'Download');
await page.getByRole('combobox', { name: 'Format' }).click();
await page.getByRole('option', { name: 'Docx' }).click();
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.docx`);
});
void page.getByTestId('doc-export-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.docx`);
});
/**
* We override the document content to ensure that the exported ODT
* contains various elements for testing.
* We don't check the content of the ODT here, just that the export works
* and the file is correctly named.
*/
test('it exports the doc to odt', async ({ page, browserName }) => {
const randomDoc = await overrideDocContent({ page, browserName });
await clickInEditorMenu(page, 'Download');
await page.getByRole('combobox', { name: 'Format' }).click();
await page.getByRole('option', { name: 'Odt' }).click();
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.odt`);
});
void page.getByTestId('doc-export-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.odt`);
});
test('it exports the doc to html zip', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(
page,
'doc-editor-html-zip',
browserName,
1,
);
await verifyDocName(page, randomDoc);
// Add some content and at least one image so that the ZIP contains media files.
await writeInEditor({ page, text: 'Hello HTML ZIP' });
await openSuggestionMenu({
page,
suggestion: 'Resizable image with caption',
});
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByText('Upload image').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, 'assets/test.svg'));
const image = page
.locator('.--docs--editor-container img.bn-visual-media')
.first();
// Wait for the image to be attached and have a valid src (aria-hidden prevents toBeVisible on Chromium)
await expect(image).toBeAttached({ timeout: 10000 });
await expect(image).toHaveAttribute('src', /.*\.svg/);
// Give some time for the image to be fully processed
await page.waitForTimeout(1000);
await clickInEditorMenu(page, 'Download');
await page.getByRole('combobox', { name: 'Format' }).click();
await page.getByRole('option', { name: 'HTML' }).click();
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.zip`);
});
void page.getByTestId('doc-export-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.zip`);
await page.waitForTimeout(1000);
const zipBuffer = await cs.toBuffer(await download.createReadStream());
// Unzip and inspect contents
const zip = await JSZip.loadAsync(zipBuffer);
// Check that index.html exists
const indexHtml = zip.file('index.html');
expect(indexHtml).not.toBeNull();
// Read and verify HTML content
const htmlContent = await indexHtml!.async('string');
expect(htmlContent).toContain('Hello HTML ZIP');
expect(htmlContent).toContain('href="styles.css"');
// Check for media files (they are at the root of the ZIP, not in a media/ folder)
// Media files are named like "1-test.svg" or "media-1.png" by deriveMediaFilename
const allFiles = Object.keys(zip.files);
const mediaFiles = allFiles.filter(
(name) => name !== 'index.html' && !name.endsWith('/'),
);
expect(mediaFiles.length).toBeGreaterThan(0);
// Verify the SVG image is included
const svgFile = mediaFiles.find((name) => name.endsWith('.svg'));
expect(svgFile).toBeDefined();
const styleFile = mediaFiles.find((name) => name === 'styles.css');
expect(styleFile).toBeDefined();
});
/**
* This test tell us that the export to pdf is working with images
* but it does not tell us if the images are being displayed correctly
* in the pdf.
*
* TODO: Check if the images are displayed correctly in the pdf
*/
test('it exports the docs with images', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
await verifyDocName(page, randomDoc);
await writeInEditor({
page,
text: 'Hello World 😃🎉🚀🙋♀️🧑🏿❤️💋🧑🏾',
});
await page.keyboard.press('Enter');
await openSuggestionMenu({ page });
await page.getByText('Resizable image with caption').click();
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByText('Upload image').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, 'assets/test.svg'));
const image = page
.locator('.--docs--editor-container img.bn-visual-media')
.first();
await expect(image).toBeVisible();
await openSuggestionMenu({
page,
suggestion: 'Resizable image with caption',
});
await page.getByRole('tab', { name: 'Embed' }).click();
await page
.getByRole('textbox', { name: 'Enter URL' })
.fill('https://docs.numerique.gouv.fr/assets/logo-gouv.png');
await page.getByText('Embed image').click();
await clickInEditorMenu(page, 'Download');
await new Promise((resolve) => setTimeout(resolve, 1000));
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
const responseCorsPromise = page.waitForResponse(
(response) =>
response.url().includes('/cors-proxy/') && response.status() === 200,
);
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
void page.getByTestId('doc-export-download-button').click();
const responseCors = await responseCorsPromise;
expect(responseCors.ok()).toBe(true);
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
await page.waitForTimeout(1000);
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
const pdfParse = new PDFParse({ data: pdfBuffer });
const pdfText = await pdfParse.getText();
expect(pdfText.text).toContain('Hello World');
});
/**
* Regression test for https://github.com/suitenumerique/docs/issues/860
*
* PNG images were silently dropped from the exported PDF because the raw
* Blob was passed directly to @react-pdf/renderer's <Image src>, which
* triggered a WASM "too many arguments" error internally and caused the
* image to be omitted. SVG images were unaffected because they were already
* converted to a data URL string before being passed to <Image>.
*/
test('it includes PNG images in the exported PDF', async ({
page,
browserName,
}) => {
// overrideDocContent uploads both an SVG and a PNG image into the editor.
await overrideDocContent({ page, browserName });
await clickInEditorMenu(page, 'Download');
const downloadPromise = page.waitForEvent('download', (download) =>
download.suggestedFilename().endsWith('.pdf'),
);
void page.getByTestId('doc-export-download-button').click();
const download = await downloadPromise;
await page.waitForTimeout(1000);
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
// Each embedded image in a PDF is stored as an XObject stream whose
// dictionary contains /Width <naturalPixelWidth>. Emoji images are 64px
// wide, the SVG (test.svg, 100×100) becomes 100px. The uploaded PNG
// (logo-suite-numerique.png) is 756px wide — a value that won't appear
// for page sizes (A4 = 595 pt) or emoji. With the bug the PNG is silently
// dropped and /Width 756 is absent; after the fix it appears twice (image
// data stream + alpha-mask stream).
const pdfString = pdfBuffer.toString('latin1');
expect(pdfString).toMatch(/\/Width 756/);
});
test('it injects the correct language attribute into PDF export', async ({
page,
browserName,
}) => {
const [randomDocFrench] = await createDoc(
page,
'doc-language-export-french',
browserName,
1,
);
await waitForLanguageSwitch(page, TestLanguage.French);
// Wait for the page to be ready after language switch
await page.waitForLoadState('domcontentloaded');
await writeInEditor({
page,
text: 'Contenu de test pour export en français',
});
await page
.getByRole('button', { name: 'Ouvrir les options du document' })
.click();
await page.getByRole('menuitem', { name: 'Télécharger' }).click();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDocFrench}.pdf`);
});
void page.getByTestId('doc-export-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDocFrench}.pdf`);
await page.waitForTimeout(1000);
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
const pdfString = pdfBuffer.toString('latin1');
expect(pdfString).toContain('/Lang (fr)');
});
test('it exports the doc to PDF with PRINT feature and checks regressions', async ({
page,
browserName,
}, testInfo) => {
// PDF Binary comparison is different depending on the browser used
// We only run this test on Chromium to avoid having to maintain
// multiple sets of PDF fixtures
if (browserName !== 'chromium') {
test.skip();
}
await overrideDocContent({ page, browserName });
await clickInEditorMenu(page, 'Download');
await page.getByRole('combobox', { name: 'Format' }).click();
await page.getByRole('option', { name: 'Print' }).click();
await page.getByRole('button', { name: 'Print' }).click();
await expect(page.locator('#print-only-content-styles')).toBeAttached();
await page.emulateMedia({ media: 'print' });
const pdfBuffer = await page.pdf({
printBackground: true,
preferCSSPageSize: true,
format: 'A4',
scale: 1,
});
// If we need to update the PDF regression fixture, uncomment the line below
// await savePDFToAssetFolder(
// pdfBuffer,
// 'doc-export-PDF-browser-regressions.pdf',
// );
// Assert the generated PDF matches the initial PDF regression fixture
await comparePDFWithAssetFolder({
originPdfBuffer: pdfBuffer,
filename: 'doc-export-PDF-browser-regressions.pdf',
compareTextContent: false,
comparePixel: false,
testInfo,
});
await expect(page.locator('#print-only-content-styles')).not.toBeAttached();
});
test('it exports the doc to PDF and checks regressions', async ({
page,
browserName,
}, testInfo) => {
// PDF Binary comparison is different depending on the browser used
// We only run this test on Chromium to avoid having to maintain
// multiple sets of PDF fixtures
if (browserName !== 'chromium') {
test.skip();
}
const randomDoc = await overrideDocContent({ page, browserName });
await clickInEditorMenu(page, 'Download');
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
await page.getByTestId('doc-export-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
await page.waitForTimeout(1000);
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
// If we need to update the PDF regression fixture, uncomment the line below
//await savePDFToAssetFolder(pdfBuffer, 'doc-export-regressions.pdf');
// Assert the generated PDF matches "assets/doc-export-regressions.pdf"
await comparePDFWithAssetFolder({
originPdfBuffer: pdfBuffer,
filename: 'doc-export-regressions.pdf',
testInfo,
});
});
});