"""Coverage for complete heading levels and rich-text page breaks.""" import json import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[1] DOC_JS = (ROOT / "static/js/document.js").read_text(encoding="utf-8") STYLE = (ROOT / "static/style.css").read_text(encoding="utf-8") def test_heading_levels_and_page_break_are_exposed_everywhere(): slash = DOC_JS.split("const _RICH_SLASH_COMMANDS", 1)[1].split( "const _RICH_BLOCK_INPUT_RULES", 1 )[0] rules = DOC_JS.split("const _RICH_BLOCK_INPUT_RULES", 1)[1].split( "function _applyRichBlockInputRule", 1 )[0] dropdown = DOC_JS.split("function _showMdDropdown", 1)[1].split( "function initMdToolbar", 1 )[0] exporter = DOC_JS.split("function _richTextExportCss", 1)[1].split( "function exportAsHtml", 1 )[0] for level in (5, 6): assert f"action: 'h{level}'" in slash assert f"['h{level}', 'Heading {level}', 'H{level}']" in dropdown assert f"h{level}: '{'#' * level} '" in DOC_JS assert "['#####', { action: 'h5' }]" in rules assert "['######', { action: 'h6' }]" in rules assert 'data-md="pagebreak"' in DOC_JS assert "action: 'pagebreak'" in slash assert "function _insertRichPageBreak" in DOC_JS assert "break-after:page" in exporter assert "page-break-after:always" in exporter assert ".doc-email-richbody.richtext-mode hr.richtext-page-break" in STYLE def test_mobile_headings_page_break_history_and_persistence(): script = r""" import { chromium } from 'playwright'; const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 390, height: 844 } }); 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&structure-tools-test=1'); window.__structureToolsDocModule = mod; mod.init('/api'); mod.injectFreshDoc({ id: 'structure-tools-doc', title: 'Structure tools', language: 'richtext', current_content: '

First page

Section detail

', version_count: 1, }); await new Promise(resolve => setTimeout(resolve, 400)); }); async function selectBlock(selector, atEnd = false) { await page.evaluate(({ selector, atEnd }) => { const block = document.querySelector(selector); const range = document.createRange(); range.selectNodeContents(block); range.collapse(!atEnd); const selection = getSelection(); selection.removeAllRanges(); selection.addRange(range); block.closest('[contenteditable]').focus(); }, { selector, atEnd }); } await selectBlock('#doc-email-richbody p:last-child'); await page.locator('[data-dd="heading"]').focus(); await page.keyboard.press('ArrowDown'); const menuLabels = await page.locator('#doc-md-dd-menu .doc-overflow-item').allTextContents(); await page.locator('#doc-md-dd-menu .doc-overflow-item').filter({ hasText: 'Heading 6' }).click(); await page.waitForTimeout(50); const headingTag = await page.locator('#doc-email-richbody').evaluate(root => root.lastElementChild.tagName); await page.evaluate(() => { const rich = document.querySelector('#doc-email-richbody'); const paragraph = document.createElement('p'); paragraph.innerHTML = '
'; rich.appendChild(paragraph); const range = document.createRange(); range.selectNodeContents(paragraph); range.collapse(true); const selection = getSelection(); selection.removeAllRanges(); selection.addRange(range); rich.focus(); }); await page.keyboard.type('#####'); await page.keyboard.press('Space'); await page.keyboard.type('Typed heading'); const typedHeading = await page.locator('#doc-email-richbody').evaluate(root => ({ tag: root.lastElementChild.tagName, text: root.lastElementChild.textContent, })); await selectBlock('#doc-email-richbody h5', true); await page.locator('[data-md="pagebreak"]').click(); await page.waitForTimeout(50); const inserted = await page.evaluate(() => ({ count: document.querySelectorAll('#doc-email-richbody hr.richtext-page-break').length, followingTag: document.querySelector('#doc-email-richbody hr.richtext-page-break')?.nextElementSibling?.tagName, stored: document.querySelector('#doc-editor-textarea').value, })); await page.locator('#doc-undo-btn').click(); await page.waitForTimeout(50); const undoCount = await page.locator('#doc-email-richbody hr.richtext-page-break').count(); await page.locator('#doc-redo-btn').click(); await page.waitForTimeout(50); const redoCount = await page.locator('#doc-email-richbody hr.richtext-page-break').count(); await page.evaluate(() => { const last = document.querySelector('#doc-email-richbody p:last-child'); last.textContent = '/page'; const range = document.createRange(); range.selectNodeContents(last); range.collapse(false); const selection = getSelection(); selection.removeAllRanges(); selection.addRange(range); last.closest('[contenteditable]').focus(); last.closest('[contenteditable]').dispatchEvent(new Event('input', { bubbles: true })); }); await page.waitForSelector('#doc-rich-slash-menu'); const slashLabels = await page.locator('#doc-rich-slash-menu .doc-rich-slash-label').allTextContents(); const geometry = await page.evaluate(() => ({ viewport: document.documentElement.clientWidth, document: document.documentElement.scrollWidth, })); await page.evaluate(stored => { window.__structureToolsDocModule.injectFreshDoc({ id: 'structure-tools-reload', title: 'Reloaded structure', language: 'richtext', current_content: stored, version_count: 1, }); }, inserted.stored); await page.waitForTimeout(100); const reloadedCount = await page.locator('#doc-email-richbody hr.richtext-page-break').count(); console.log(JSON.stringify({ menuLabels, headingTag, typedHeading, inserted, undoCount, redoCount, slashLabels, geometry, reloadedCount, })); await browser.close(); """ result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, check=False, capture_output=True, text=True, ) assert result.returncode == 0, result.stderr data = json.loads(result.stdout) assert any("Heading 5" in label for label in data["menuLabels"]) assert any("Heading 6" in label for label in data["menuLabels"]) assert data["headingTag"] == "H6" assert data["typedHeading"] == {"tag": "H5", "text": "Typed heading"} assert data["inserted"]["count"] == 1 assert data["inserted"]["followingTag"] == "P" assert 'class="richtext-page-break"' in data["inserted"]["stored"] assert "data-editor-page-break-token" not in data["inserted"]["stored"] assert data["undoCount"] == 0 assert data["redoCount"] == 1 assert data["reloadedCount"] == 1 assert "Page break" in data["slashLabels"] assert data["geometry"]["document"] == data["geometry"]["viewport"]