"""Browser coverage for undoable rich-text table header controls.""" 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") def test_table_header_controls_use_tag_replacement_and_native_history(): table_actions = DOC_JS.split("function _applyRichTableAction", 1)[1].split( "function applyMdFormat", 1 )[0] menu_state = DOC_JS.split("function _richDropdownCurrentActions", 1)[1].split( "function _showMdDropdown", 1 )[0] assert "function _replaceRichTableCellTag" in DOC_JS assert "table:toggle-header-row" in table_actions assert "table:toggle-header-column" in table_actions assert "const headerModes = _richTableHeaderModes(clone)" in table_actions assert "headerModes.headerRow = !headerModes.headerRow" in table_actions assert "headerModes.headerColumn = !headerModes.headerColumn" in table_actions assert "_applyRichTableHeaderModes(clone, headerModes)" in table_actions header_normalizer = DOC_JS.split("function _applyRichTableHeaderModes", 1)[1].split( "function _replaceRichTable", 1 )[0] assert "_replaceRichTableCellTag" in header_normalizer assert "_replaceRichTable(rich, original, clone" in table_actions assert "current.add('table:toggle-header-row')" in menu_state assert "current.add('table:toggle-header-column')" in menu_state assert "tableHeaderToggle" in DOC_JS def test_mobile_header_row_and_column_toggle_independently_with_undo(): 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&table-header-test=1'); mod.init('/api'); mod.injectFreshDoc({ id: 'table-header-doc', title: 'Table headers', language: 'richtext', current_content: '
NameValue
One1

After

', version_count: 1, }); await new Promise(resolve => setTimeout(resolve, 450)); const cell = document.querySelector('#doc-email-richbody tr:last-child td:first-child'); const range = document.createRange(); range.selectNodeContents(cell); range.collapse(true); const selection = getSelection(); selection.removeAllRanges(); selection.addRange(range); cell.closest('[contenteditable]').focus(); }); async function openTableMenu() { await page.locator('[data-dd="table"]').focus(); await page.keyboard.press('ArrowDown'); await page.waitForTimeout(30); } async function state() { return page.evaluate(() => ({ tags: Array.from(document.querySelectorAll('#doc-email-richbody tr')).map(row => Array.from(row.cells).map(cell => cell.tagName) ), checks: Object.fromEntries(Array.from(document.querySelectorAll('#doc-md-dd-menu [aria-checked]')).map(item => [ item.textContent.trim(), item.getAttribute('aria-checked'), ])), roles: Array.from(document.querySelectorAll('#doc-md-dd-menu [aria-checked]')).map(item => item.getAttribute('role')), })); } await openTableMenu(); const initial = await state(); const menuRect = await page.locator('#doc-md-dd-menu').evaluate(menu => { const rect = menu.getBoundingClientRect(); return { left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom }; }); await page.locator('#doc-md-dd-menu .doc-overflow-item').filter({ hasText: 'Header column' }).click(); await page.waitForTimeout(50); const columnOn = await page.evaluate(() => ({ tags: Array.from(document.querySelectorAll('#doc-email-richbody tr')).map(row => Array.from(row.cells).map(cell => cell.tagName)), undoDisabled: document.querySelector('#doc-undo-btn').disabled, })); await page.locator('#doc-undo-btn').click(); await page.waitForTimeout(50); const undone = await page.evaluate(() => Array.from(document.querySelectorAll('#doc-email-richbody tr')).map(row => Array.from(row.cells).map(cell => cell.tagName) )); await page.locator('#doc-redo-btn').click(); await page.waitForTimeout(50); const redone = await page.evaluate(() => Array.from(document.querySelectorAll('#doc-email-richbody tr')).map(row => Array.from(row.cells).map(cell => cell.tagName) )); await openTableMenu(); const bothOn = await state(); await page.locator('#doc-md-dd-menu .doc-overflow-item').filter({ hasText: 'Header row' }).click(); await page.waitForTimeout(50); await openTableMenu(); const rowOff = await state(); await page.keyboard.press('Escape'); const overflow = await page.evaluate(() => ({ scrollWidth: document.documentElement.scrollWidth, clientWidth: document.documentElement.clientWidth, })); console.log(JSON.stringify({ initial, menuRect, columnOn, undone, redone, bothOn, rowOff, overflow })); 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) header_checks = lambda state: { label: value for label, value in state["checks"].items() if "Header" in label } assert data["initial"]["tags"] == [["TH", "TH"], ["TD", "TD"]] assert header_checks(data["initial"]) == {"H↔Header row": "true", "H↕Header column": "false"} assert data["initial"]["roles"].count("menuitemcheckbox") == 2 assert data["initial"]["roles"].count("menuitemradio") == 3 assert data["menuRect"]["left"] >= 8 assert data["menuRect"]["right"] <= 382 assert data["menuRect"]["top"] >= 8 assert data["menuRect"]["bottom"] <= 836 assert data["columnOn"]["tags"] == [["TH", "TH"], ["TH", "TD"]] assert data["columnOn"]["undoDisabled"] is False assert data["undone"] == [["TH", "TH"], ["TD", "TD"]] assert data["redone"] == [["TH", "TH"], ["TH", "TD"]] assert header_checks(data["bothOn"]) == {"H↔Header row": "true", "H↕Header column": "true"} assert data["rowOff"]["tags"] == [["TH", "TD"], ["TH", "TD"]] assert header_checks(data["rowOff"]) == {"H↔Header row": "false", "H↕Header column": "true"} assert data["overflow"]["scrollWidth"] == data["overflow"]["clientWidth"]