diff --git a/CHANGELOG.md b/CHANGELOG.md
index 27a4d7e4..86546ee6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,10 +6,15 @@ and this project adheres to
## [Unreleased]
+### Added
+
+✨(frontend) Can print a doc #1832
+
### Changed
- ♿️(frontend) Focus main container after navigation #1854
+
### Fixed
🐛(frontend) fix broadcast store sync #1846
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/assets/doc-export-PDF-browser-regressions.pdf b/src/frontend/apps/e2e/__tests__/app-impress/assets/doc-export-PDF-browser-regressions.pdf
new file mode 100644
index 00000000..19240225
Binary files /dev/null and b/src/frontend/apps/e2e/__tests__/app-impress/assets/doc-export-PDF-browser-regressions.pdf differ
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts
index e6e76a98..6cf92154 100644
--- a/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts
+++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts
@@ -1,7 +1,7 @@
import fs from 'fs';
import path from 'path';
-import { Download, Page, expect, test } from '@playwright/test';
+import { Page, expect, test } from '@playwright/test';
import cs from 'convert-stream';
import JSZip from 'jszip';
import { PDFParse } from 'pdf-parse';
@@ -33,7 +33,9 @@ test.describe('Doc Export', () => {
await expect(page.getByTestId('modal-export-title')).toBeVisible();
await expect(
- page.getByText(/Download your document in a \.docx, \.odt.*format\./i),
+ 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(
@@ -306,6 +308,50 @@ test.describe('Doc Export', () => {
expect(pdfString).toContain('/Lang (fr)');
});
+ test('it exports the doc to PDF with PRINT feature and checks regressions', async ({
+ page,
+ browserName,
+ }) => {
+ await overrideDocContent({ page, browserName });
+
+ await page
+ .getByRole('button', {
+ name: 'Export the document',
+ })
+ .click();
+
+ 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(
+ pdfBuffer,
+ 'doc-export-PDF-browser-regressions.pdf',
+ false,
+ );
+
+ await expect(page.locator('#print-only-content-styles')).not.toBeAttached();
+ });
+
test('it exports the doc to PDF and checks regressions', async ({
page,
browserName,
@@ -325,10 +371,6 @@ test.describe('Doc Export', () => {
})
.click();
- await expect(
- page.getByTestId('doc-open-modal-download-button'),
- ).toBeVisible();
-
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
@@ -339,28 +381,29 @@ test.describe('Doc Export', () => {
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
// If we need to update the PDF regression fixture, uncomment the line below
- //await savePDFToAssetFolder(download);
+ const pdfBuffer = await cs.toBuffer(await download.createReadStream());
+ //await savePDFToAssetFolder(pdfBuffer, 'doc-export-regressions.pdf');
// Assert the generated PDF matches "assets/doc-export-regressions.pdf"
- await comparePDFWithAssetFolder(download);
+ await comparePDFWithAssetFolder(pdfBuffer, 'doc-export-regressions.pdf');
});
});
-export const savePDFToAssetFolder = async (download: Download) => {
- const pdfBuffer = await cs.toBuffer(await download.createReadStream());
- const pdfPath = path.join(__dirname, 'assets', `doc-export-regressions.pdf`);
+export const savePDFToAssetFolder = async (
+ pdfBuffer: Buffer,
+ filename: string,
+) => {
+ const pdfPath = path.join(__dirname, 'assets', filename);
fs.writeFileSync(pdfPath, pdfBuffer);
};
-export const comparePDFWithAssetFolder = async (download: Download) => {
- const pdfBuffer = await cs.toBuffer(await download.createReadStream());
-
+export const comparePDFWithAssetFolder = async (
+ pdfBuffer: Buffer,
+ filename: string,
+ compareTextContent = true,
+) => {
// Load reference PDF for comparison
- const referencePdfPath = path.join(
- __dirname,
- 'assets',
- 'doc-export-regressions.pdf',
- );
+ const referencePdfPath = path.join(__dirname, 'assets', filename);
const referencePdfBuffer = fs.readFileSync(referencePdfPath);
@@ -387,8 +430,16 @@ export const comparePDFWithAssetFolder = async (download: Download) => {
// Compare page count
expect(generatedInfo.total).toBe(referenceInfo.total);
- // Compare text content
- expect(generatedText.text).toBe(referenceText.text);
+ /*
+ Compare text content
+ We make this optional because text extraction from PDFs can vary
+ slightly between environments and PDF versions, leading to false negatives.
+ Particularly with emojis which can be represented differently when
+ exporting or parsing the PDF.
+ */
+ if (compareTextContent) {
+ expect(generatedText.text).toBe(referenceText.text);
+ }
// Compare screenshots page by page
for (let i = 0; i < generatedScreenshot.pages.length; i++) {
diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx
index 1501f81b..4f03be15 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx
@@ -34,12 +34,14 @@ import {
generateHtmlDocument,
improveHtmlAccessibility,
} from '../utils_html';
+import { printDocumentWithStyles } from '../utils_print';
enum DocDownloadFormat {
HTML = 'html',
PDF = 'pdf',
DOCX = 'docx',
ODT = 'odt',
+ PRINT = 'print',
}
interface ModalExportProps {
@@ -66,6 +68,14 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
setIsExporting(true);
+ // Handle print separately as it doesn't download a file
+ if (format === DocDownloadFormat.PRINT) {
+ printDocumentWithStyles();
+ setIsExporting(false);
+ onClose();
+ return;
+ }
+
const filename = (doc.title || untitledDocument)
.toLowerCase()
.normalize('NFD')
@@ -199,13 +209,15 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
>
}
@@ -225,7 +237,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
$align="flex-start"
data-testid="modal-export-title"
>
- {t('Download')}
+ {t('Export')}
{
>
{t(
- 'Download your document in a .docx, .odt, .pdf or .html(zip) format.',
+ 'Export your document to print or download in .docx, .odt, .pdf or .html(zip) format.',
)}