"""Artifact-level coverage for structured Rich Text Word export."""
import json
import subprocess
import tempfile
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DOC_JS = (ROOT / "static/js/document.js").read_text(encoding="utf-8")
def test_rich_docx_converter_maps_editor_structure_instead_of_raw_html():
converter = DOC_JS.split("function _docxHexColor", 1)[1].split(
"async function exportAsDocx", 1
)[0]
exporter = DOC_JS.split("async function exportAsDocx", 1)[1].split(
"/** Delete the active document", 1
)[0]
for helper in (
"_docxImageFallbackRun",
"_docxImageWidth",
"_docxImagePngData",
"_docxPrepareImages",
"_docxInlineChildren",
"_docxParagraphFromElement",
"_docxListBlocks",
"_docxTableBlock",
"_docxFigureBlocks",
"_docxBlocksFromNodes",
"_richTextToDocxChildren",
"_markdownToDocxChildren",
):
assert f"function {helper}" in DOC_JS
assert "new docx.ExternalHyperlink" in converter
assert "new docx.ImageRun" in converter
assert "new docx.PageBreak" in converter
assert "new docx.Paragraph({ thematicBreak: true })" in converter
assert "new docx.Table(" in converter
assert "new docx.TableRow(" in converter
assert "new docx.TableCell(" in converter
assert "docx.VerticalAlign?.CENTER" in converter
assert "docx.HeadingLevel.HEADING_6" in converter
assert "_isRichTextLang(lang)" in exporter
assert "_richTextToDocxChildren(text, window.docx)" in exporter
assert "_markdownToDocxChildren(text, window.docx)" in exporter
def test_browser_word_export_contains_native_rich_docx_ooxml():
with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as output:
output_path = Path(output.name)
script = rf"""
import {{ chromium }} from 'playwright';
const browser = await chromium.launch({{ headless: true }});
const page = await browser.newPage({{
viewport: {{ width: 900, height: 700 }},
acceptDownloads: true,
}});
await page.goto('http://127.0.0.1:7011/static/js/documentStats.js');
await page.route('**/api/upload/docx-image-test', route => route.fulfill({{
status: 200,
contentType: 'image/png',
body: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64'),
}}));
await page.setContent('
');
await page.evaluate(async () => {{
const mod = await import('/static/js/document.js?v=20260831richtexttools91&docx-export-test=1');
mod.init('/api');
mod.injectFreshDoc({{
id: 'rich-docx-export-doc',
title: 'Rich export',
language: 'richtext',
version_count: 1,
current_content: 'Project brief
Bold, italic, underlined, struck, and linked.
- First item
- Second item

Figure caption
Second page
',
}});
await new Promise(resolve => setTimeout(resolve, 450));
}});
await page.locator('#doc-footer-export-btn').click();
await page.waitForSelector('#doc-export-menu');
const downloadPromise = page.waitForEvent('download');
await page.locator('#doc-export-menu .doc-overflow-item').filter({{ hasText: 'Export as Word' }}).click();
const download = await downloadPromise;
await download.saveAs({json.dumps(str(output_path))});
console.log(JSON.stringify({{
filename: download.suggestedFilename(),
failure: await download.failure(),
}}));
await browser.close();
"""
result = subprocess.run(
["node", "--input-type=module", "-e", script],
cwd=ROOT,
check=False,
capture_output=True,
text=True,
)
try:
assert result.returncode == 0, result.stderr
data = json.loads(result.stdout)
assert data["failure"] is None
assert data["filename"].endswith(".docx")
assert output_path.stat().st_size > 5000
with zipfile.ZipFile(output_path) as package:
document_xml = package.read("word/document.xml").decode("utf-8")
numbering_xml = package.read("word/numbering.xml").decode("utf-8")
relationships_xml = package.read("word/_rels/document.xml.rels").decode("utf-8")
media_names = [
name for name in package.namelist()
if name.startswith("word/media/") and not name.endswith("/")
]
media = package.read(media_names[0]) if media_names else b""
assert '<h1>' not in document_xml
assert '' in document_xml
assert '' in document_xml
for text in (
"Project brief", "Bold", "italic", "underlined", "struck", "linked",
"First item", "Second item", "[x] ", "Finished task", "Name", "Status",
"Figure caption", "Alpha", "Ready", "Second page",
):
assert text in document_xml
assert "" in document_xml
assert "" in document_xml
assert '' in document_xml
assert "" in document_xml
assert "" in document_xml
assert '' in numbering_xml
assert "" in document_xml
assert "" in document_xml
assert '' in document_xml
assert "" in document_xml
assert '' in document_xml
assert len(media_names) == 1
assert media.startswith(b"\x89PNG\r\n\x1a\n")
assert "relationships/image" in relationships_xml
assert "" in document_xml
assert '' in document_xml
assert '' in document_xml
assert 'descr="Export diagram"' in document_xml
assert "[Image: Export diagram]" not in document_xml
finally:
output_path.unlink(missing_ok=True)
def test_browser_markdown_word_export_keeps_heading_and_inline_formatting():
with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as output:
output_path = Path(output.name)
script = rf"""
import {{ chromium }} from 'playwright';
const browser = await chromium.launch({{ headless: true }});
const page = await browser.newPage({{
viewport: {{ width: 900, height: 700 }},
acceptDownloads: true,
}});
await page.goto('http://127.0.0.1:7011/static/js/documentStats.js');
await page.setContent('');
await page.evaluate(async () => {{
const mod = await import('/static/js/document.js?v=20260831richtexttools91&markdown-docx-export-test=1');
mod.init('/api');
mod.injectFreshDoc({{
id: 'markdown-docx-export-doc',
title: 'Markdown export',
language: 'markdown',
version_count: 1,
current_content: '# Markdown title\n\nA **bold phrase** and *italic phrase*.',
}});
await new Promise(resolve => setTimeout(resolve, 450));
}});
await page.locator('#doc-footer-export-btn').click();
await page.waitForSelector('#doc-export-menu');
const downloadPromise = page.waitForEvent('download');
await page.locator('#doc-export-menu .doc-overflow-item').filter({{ hasText: 'Export as Word' }}).click();
const download = await downloadPromise;
await download.saveAs({json.dumps(str(output_path))});
console.log(JSON.stringify({{ failure: await download.failure() }}));
await browser.close();
"""
result = subprocess.run(
["node", "--input-type=module", "-e", script],
cwd=ROOT,
check=False,
capture_output=True,
text=True,
)
try:
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout)["failure"] is None
with zipfile.ZipFile(output_path) as package:
document_xml = package.read("word/document.xml").decode("utf-8")
assert '' in document_xml
assert "Markdown title" in document_xml
assert "bold phrase" in document_xml
assert "italic phrase" in document_xml
assert "" in document_xml
assert "" in document_xml
finally:
output_path.unlink(missing_ok=True)