mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 15:02:20 +02:00
static/js/document.js is 17,579 lines and is about to be decomposed behind a
re-export wrapper. 51 test files read it off disk and grep it as text, and 28
of those slice it with `src.split("function a", 1)[1].split("function b", 1)[0]`
-- "the region between a and b", which only means what the test intends while a
and b are neighbours in one file. Several also hard-code the file's two-space
indentation, which no extracted module reproduces. Left alone, the first
extraction makes those assertions cover the wrong region, and an `x in region`
check passes while covering more than it was written for.
tests/helpers/document_source is the one place that names the file now:
- document_source() is the entry plus everything under static/js/document/,
so a membership assertion keeps finding its subject wherever it lands;
- function_body()/declaration() locate a construct by name in whichever
module defines it and end at its real closing brace, so neither moving it
nor moving its neighbour changes the region.
The rewrite only collapses a slice when the old terminator sat at the
construct's end. 28 slices deliberately span a whole family of functions --
everything from _docxHexColor to exportAsDocx -- and collapsing one to its
first member drops what the assertions look for, so those stay as they are and
are listed in KNOWN_ADJACENCY_SLICES, to be converted as each family becomes a
module. That list may only shrink.
Two guards come with it:
- test_document_source_test_hygiene fails on a direct read of the entry file
and on any new adjacency slice;
- test_frontend_module_graph resolves every relative import under static/
(718 of them, none broken today) and requires the document module set to
stay in the sw.js precache, since the worker fetches the URLs it lists and
not what they import.
test_document_module_api pins the 38 default-export keys and 29 named exports
by loading the module in a browser and reading what it actually exports, rather
than grepping for the literal object -- after extraction that object may be
assembled from imports, and a source-shape check would pass while the export
was broken.
No JavaScript moves here. static/ is untouched.
172 lines
7.5 KiB
Python
172 lines
7.5 KiB
Python
"""Coverage for complete heading levels and rich-text page breaks."""
|
|
|
|
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
from tests.helpers.document_source import declaration, document_source, function_body
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
DOC_JS = document_source()
|
|
STYLE = (ROOT / "static/style.css").read_text(encoding="utf-8")
|
|
|
|
|
|
def test_heading_levels_and_page_break_are_exposed_everywhere():
|
|
slash = declaration("_RICH_SLASH_COMMANDS")
|
|
rules = declaration("_RICH_BLOCK_INPUT_RULES")
|
|
dropdown = DOC_JS.split("function _showMdDropdown", 1)[1].split(
|
|
"function initMdToolbar", 1
|
|
)[0]
|
|
exporter = function_body("_richTextExportCss")
|
|
|
|
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(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
|
|
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
|
|
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: '<p>First page</p><p>Section detail</p>',
|
|
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 = '<br>';
|
|
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"]
|