"""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"]