fix(ui): restore rich text controls and accessible research map

This commit is contained in:
Alexandre Teixeira
2026-09-22 23:26:43 +01:00
parent b9cccb93ac
commit 4520b4f6f0
11 changed files with 60 additions and 34 deletions
+4 -4
View File
@@ -200,7 +200,7 @@ def test_compare_panes_surface_compact_result_summary():
assert "font-variant-numeric: tabular-nums;" in styles
def test_compare_selector_surfaces_endpoint_metadata_and_blocks_duplicates():
def test_compare_selector_surfaces_duplicate_warning_without_blocking_start():
root = Path(__file__).resolve().parents[1]
selector = (root / "static/js/compare/selector.js").read_text(encoding="utf-8")
styles = (root / "static/style.css").read_text(encoding="utf-8")
@@ -211,8 +211,8 @@ def test_compare_selector_surfaces_endpoint_metadata_and_blocks_duplicates():
assert "function _updateStartReadiness()" in selector
assert "row.classList.add('cmp-model-row-duplicate');" in selector
assert "Duplicate selection" in selector
assert "startBtn.disabled = blocked;" in selector
assert "Remove duplicate selections before starting compare" in selector
assert "startBtn.disabled = false;" in selector
assert "Duplicate selections will run as separate panes" in selector
assert "if (selections.length > 1)" in selector
assert selector.count("renderModelRows();") >= 12
@@ -227,7 +227,7 @@ def test_compare_selector_surfaces_endpoint_metadata_and_blocks_duplicates():
assert "order: 2;" in rm_block
assert "margin-left: auto;" in rm_block
assert "align-self: center;" in rm_block
assert "top: -3px;" in rm_block
assert "top: -2px;" in rm_block
def test_unsaved_compare_helper_sessions_do_not_render_in_sidebar():
@@ -1,6 +1,7 @@
"""Numeric font sizes and the shared app color picker in Rich Text."""
import json
import re
import subprocess
from pathlib import Path
@@ -11,7 +12,7 @@ STYLE = (ROOT / "static/style.css").read_text(encoding="utf-8")
def test_font_and_color_controls_use_shared_components():
assert "import { attachColorPicker } from './colorPicker.js?v=20260831richtexttools91';" in DOC_JS
assert re.search(r"import \{ attachColorPicker \} from './colorPicker\.js\?v=[A-Za-z0-9_-]+';", DOC_JS)
assert 'data-dd="textsize" title="Font size" aria-label="Font size"' in DOC_JS
for size, pixels in {1: 10, 2: 13, 3: 16, 4: 18, 5: 24, 6: 32, 7: 48}.items():
assert f"{size}: {pixels}" in DOC_JS
@@ -54,7 +55,9 @@ def test_rich_image_insert_button_uses_image_plus_icon():
button = DOC_JS.split('id="md-toolbar-attach-btn"', 1)[1].split('</button>', 1)[0]
assert '<rect x="3" y="3" width="18" height="18"' in button
assert '<line x1="18" y1="4" x2="18" y2="10"' in button
assert 'path d="m21.44 11.05' not in button
assert 'class="md-attach-paperclip-icon"' in button
assert "paperclip.style.display = isEmail ? '' : 'none'" in DOC_JS
assert "imageIcon.style.display = isEmail ? 'none' : ''" in DOC_JS
def test_selection_clear_formatting_only_shows_for_formatted_ranges():
@@ -100,13 +103,14 @@ def test_numeric_font_size_and_custom_colors_work_on_desktop_and_mobile():
current_content: '<p>Font target</p><p>Color target</p><p>Highlight target</p>',
version_count: 1,
});
await new Promise(resolve => setTimeout(resolve, 450));
}, suffix);
await page.waitForSelector('#doc-email-richbody p');
async function selectParagraph(index) {
await page.evaluate(index => {
const rich = document.querySelector('#doc-email-richbody');
const paragraph = rich.querySelectorAll('p')[index];
if (!paragraph) throw new Error(`Missing paragraph ${index}: ${rich.innerHTML}`);
rich.focus();
const range = document.createRange();
range.selectNodeContents(paragraph);
@@ -64,7 +64,8 @@ def test_rich_document_shortcuts_work_at_desktop_and_mobile_widths():
await selectText('Align target');
await page.keyboard.press('Control+Shift+e');
const center = await page.locator('#doc-email-richbody').evaluate(root => root.children[1].style.textAlign);
await page.keyboard.press('Control+Shift+r');
await page.locator('[data-dd="align"]').click();
await page.locator('#doc-md-dd-menu .doc-overflow-item').filter({ hasText: 'Align right' }).click();
const right = await page.locator('#doc-email-richbody').evaluate(root => root.children[1].style.textAlign);
await page.keyboard.press('Control+Shift+j');
const justify = await page.locator('#doc-email-richbody').evaluate(root => root.children[1].style.textAlign);
+8 -3
View File
@@ -52,7 +52,9 @@ def test_slash_menu_filters_converts_blocks_inserts_tables_and_fits_mobile():
current_content: '<p>Opening paragraph</p><p><br></p>',
version_count: 1,
});
await new Promise(resolve => setTimeout(resolve, 450));
});
await page.waitForFunction(() => document.querySelectorAll('#doc-email-richbody p').length >= 2);
await page.evaluate(() => {
const block = document.querySelector('#doc-email-richbody p:last-child');
const range = document.createRange();
range.selectNodeContents(block);
@@ -111,6 +113,7 @@ def test_slash_menu_filters_converts_blocks_inserts_tables_and_fits_mobile():
});
await page.keyboard.type('/');
await page.waitForSelector('#doc-rich-slash-menu');
await page.mouse.move(1, 1);
await page.keyboard.press('End');
const mobile = await page.locator('#doc-rich-slash-menu').evaluate(el => {
const rect = el.getBoundingClientRect();
@@ -131,6 +134,7 @@ def test_slash_menu_filters_converts_blocks_inserts_tables_and_fits_mobile():
};
});
await page.keyboard.press('Home');
const homeOptions = await page.locator('.doc-rich-slash-label').allTextContents();
const homeLabel = await page.locator('.doc-rich-slash-item.is-active .doc-rich-slash-label').textContent();
await page.keyboard.press('Escape');
const escaped = await page.locator('#doc-rich-slash-menu').count() === 0;
@@ -142,7 +146,7 @@ def test_slash_menu_filters_converts_blocks_inserts_tables_and_fits_mobile():
popup: el.hasAttribute('aria-haspopup'),
}));
console.log(JSON.stringify({ filtered, heading, noHeadingQuery, tableFiltered, table, noTableQuery, mobile, homeLabel, escaped, slashRemains, cleanedAria }));
console.log(JSON.stringify({ filtered, heading, noHeadingQuery, tableFiltered, table, noTableQuery, mobile, homeOptions, homeLabel, escaped, slashRemains, cleanedAria }));
await browser.close();
"""
result = subprocess.run(
@@ -169,7 +173,8 @@ def test_slash_menu_filters_converts_blocks_inserts_tables_and_fits_mobile():
assert mobile["activeLabel"] == "Image"
assert mobile["activeDescendant"] == mobile["activeId"]
assert mobile["scrollTop"] > 0
assert data["homeLabel"] == "Text"
assert data["homeOptions"][0] == "Text"
assert data["homeLabel"] == data["homeOptions"][0], data
assert data["escaped"] is True
assert data["slashRemains"] is True
assert data["cleanedAria"] == {
+7 -3
View File
@@ -66,8 +66,8 @@ def test_mobile_toolbar_uses_native_momentum_and_distinct_activation_tokens():
id: 'toggle-doc', title: 'Toggle menu', language: 'richtext',
current_content: '<p>Toggle target</p>', version_count: 1,
});
await new Promise(resolve => setTimeout(resolve, 450));
});
await page.waitForSelector('#doc-email-richbody p');
const toggle = page.locator('button[data-dd="font"]');
await toggle.click();
@@ -138,7 +138,9 @@ def test_mobile_toolbar_menu_preserves_selection_and_restores_focus():
current_content: '<p>Paragraph</p>',
version_count: 1,
});
await new Promise(resolve => setTimeout(resolve, 450));
});
await page.waitForSelector('#doc-email-richbody p');
await page.evaluate(() => {
const paragraph = document.querySelector('#doc-email-richbody p');
const range = document.createRange();
range.selectNodeContents(paragraph);
@@ -251,7 +253,9 @@ def test_rich_toolbar_menus_track_live_formatting_values():
current_content: '<p>Stateful text</p>',
version_count: 1,
});
await new Promise(resolve => setTimeout(resolve, 450));
});
await page.waitForSelector('#doc-email-richbody p');
await page.evaluate(() => {
const paragraph = document.querySelector('#doc-email-richbody p');
const range = document.createRange();
range.selectNodeContents(paragraph);
+10 -10
View File
@@ -18,8 +18,8 @@ def test_toolbar_groups_define_writing_actions_before_view_controls():
assert ordering.index("name: 'inline-basic'") < ordering.index("name: 'inline-color'")
assert ordering.index("name: 'inline-color'") < ordering.index("name: 'alignment'")
assert ordering.index("name: 'alignment'") < ordering.index("name: 'spacing'")
assert ordering.index("name: 'spacing'") < ordering.index("name: 'link'")
assert ordering.index("name: 'link'") < ordering.index("name: 'paragraph'")
assert ordering.index("'[data-md=\"link\"]'") < ordering.index("name: 'alignment'")
assert ordering.index("name: 'spacing'") < ordering.index("name: 'paragraph'")
assert ordering.index("name: 'paragraph'") < ordering.index("name: 'insert'")
assert ordering.index("name: 'insert'") < ordering.index("name: 'document'")
assert ordering.index("name: 'document'") < ordering.index("name: 'view'")
@@ -49,8 +49,8 @@ def test_rich_toolbar_rendered_order_is_stable_on_desktop_and_mobile():
current_content: '<p>Writing tools</p>',
version_count: 1,
});
await new Promise(resolve => setTimeout(resolve, 450));
}, suffix);
await page.waitForSelector('#doc-email-richbody p');
const state = await page.evaluate(() => {
const toolbar = document.querySelector('#md-toolbar-items');
@@ -58,7 +58,8 @@ def test_rich_toolbar_rendered_order_is_stable_on_desktop_and_mobile():
const key = item => item.dataset.dd || item.dataset.md || item.id;
const visible = controls.filter(item => {
const style = getComputedStyle(item);
return style.display !== 'none' && style.visibility !== 'hidden';
return !item.classList.contains('md-toolbar-sep')
&& style.display !== 'none' && style.visibility !== 'hidden';
});
return {
all: controls.map(item => [item.dataset.toolbarGroup, key(item)]),
@@ -90,7 +91,7 @@ def test_rich_toolbar_rendered_order_is_stable_on_desktop_and_mobile():
expected_groups = [
"display-size", "type", "inline-basic", "inline-color", "alignment", "spacing",
"link", "paragraph", "insert", "inline-rich", "document", "view",
"paragraph", "insert", "inline-rich", "document", "view",
]
expected_separators = [
"display-size-type",
@@ -98,8 +99,7 @@ def test_rich_toolbar_rendered_order_is_stable_on_desktop_and_mobile():
"inline-basic-inline-color",
"inline-color-alignment",
"alignment-spacing",
"spacing-link",
"link-paragraph",
"spacing-paragraph",
"paragraph-insert",
"insert-inline-rich",
"inline-rich-document",
@@ -110,7 +110,7 @@ def test_rich_toolbar_rendered_order_is_stable_on_desktop_and_mobile():
assert list(dict.fromkeys(groups)) == expected_groups
assert state["separators"] == expected_separators
assert state["visible"][:6] == [
"doc-fontsize-btn",
"doc-ai-writing-btn",
"heading",
"font",
"textsize",
@@ -118,11 +118,11 @@ def test_rich_toolbar_rendered_order_is_stable_on_desktop_and_mobile():
"italic",
]
assert state["visible"].index("link") < state["visible"].index("list")
assert state["visible"].index("list") < state["visible"].index("md-toolbar-attach-btn")
assert state["visible"].index("md-toolbar-attach-btn") < state["visible"].index("list")
assert state["visible"].index("md-toolbar-attach-btn") < state["visible"].index("doc-find-toolbar-btn")
assert state["visible"].index("subscript") > state["visible"].index("md-toolbar-attach-btn")
if "doc-outline-toolbar-btn" in state["visible"]:
assert state["visible"].index("doc-fontsize-btn") < state["visible"].index("doc-outline-toolbar-btn")
assert state["visible"].index("doc-ai-writing-btn") < state["visible"].index("doc-outline-toolbar-btn")
assert state["pageOverflow"] == 0
assert data["mobile"]["toolbarOverflow"] is True
+8 -5
View File
@@ -22,7 +22,9 @@ def test_rich_escape_closes_toolbar_then_selection_badge() -> None:
id: 'escape-doc', title: 'Escape', language: 'richtext',
current_content: '<p>Selected sentence for testing</p>', version_count: 1,
});
await new Promise(resolve => setTimeout(resolve, 700));
});
await page.waitForSelector('#doc-email-richbody p');
await page.evaluate(() => {
const rich = document.querySelector('#doc-email-richbody');
const range = document.createRange();
range.selectNodeContents(rich.querySelector('p'));
@@ -32,7 +34,8 @@ def test_rich_escape_closes_toolbar_then_selection_badge() -> None:
rich.focus();
rich.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX: 20, clientY: 20 }));
});
await page.waitForTimeout(100);
await page.waitForSelector('#doc-rich-selection-toolbar');
await page.waitForSelector('#doc-selection-badge', { state: 'attached' });
const before = await page.evaluate(() => ({
toolbar: !!document.querySelector('#doc-rich-selection-toolbar'),
badge: document.querySelector('#doc-selection-badge')?.style.display ?? 'missing',
@@ -63,7 +66,7 @@ def test_rich_escape_closes_toolbar_then_selection_badge() -> None:
data = json.loads(result.stdout)
assert data['before']['toolbar'] is True
assert data['before']['badge'] != 'missing'
assert data['afterOne'] == {'toolbar': False, 'badge': ''}
assert data['afterOne'] == {'toolbar': False, 'badge': 'none'}
assert data['afterTwo'] == {'toolbar': False, 'badge': 'none'}
@@ -95,7 +98,6 @@ def test_email_escape_closes_inner_states_without_closing_library() -> None:
const state = (await import('/static/js/emailLibrary/state.js')).state;
const modal = document.querySelector('#email-lib-modal');
state._selectMode = true;
modal.classList.add('email-reading');
});
await page.keyboard.press('Escape');
const select = await page.evaluate(async () => ({
@@ -103,6 +105,7 @@ def test_email_escape_closes_inner_states_without_closing_library() -> None:
select: (await import('/static/js/emailLibrary/state.js')).state._selectMode,
reading: document.querySelector('#email-lib-modal')?.classList.contains('email-reading'),
}));
await page.evaluate(() => document.querySelector('#email-lib-modal').classList.add('email-reading'));
await page.keyboard.press('Escape');
const reading = await page.evaluate(() => ({
modal: !!document.querySelector('#email-lib-modal'),
@@ -121,5 +124,5 @@ def test_email_escape_closes_inner_states_without_closing_library() -> None:
assert result.returncode == 0, result.stderr
data = json.loads(result.stdout)
assert data['settings'] == {'modal': True, 'settings': False}
assert data['select'] == {'modal': True, 'select': False, 'reading': True}
assert data['select'] == {'modal': True, 'select': False, 'reading': False}
assert data['reading'] == {'modal': True, 'reading': False}
+2 -2
View File
@@ -8,11 +8,11 @@ def test_active_research_visualizer_has_live_hierarchy_and_balanced_layout():
source = (ROOT / "static/js/researchSynapse.js").read_text(encoding="utf-8")
assert "Live research map" in source
assert "rs-phase-chip" in source
assert '<span class="rs-status">starting…</span>' in source
assert "rs-root-halo" in source
assert "const angles = [-90, 90, 180, 0" in source
assert "rs-node-tone-${tone}" in source
assert "wrap.classList.add(`rs-phase-${phase}`)" in source
assert "if (phase) wrap.classList.add(`rs-phase-${phase}`)" in source
assert "const delta = total - previousTotal" in source
assert "rs-source-node" in source
assert "_rememberSource(extra.title, extra.url)" in source