diff --git a/static/js/document.js b/static/js/document.js index f7e01702f..f5964277a 100644 --- a/static/js/document.js +++ b/static/js/document.js @@ -7521,6 +7521,7 @@ import { attachColorPicker } from './colorPicker.js?v=20260910eyedropper1'; + @@ -7717,6 +7718,7 @@ import { attachColorPicker } from './colorPicker.js?v=20260910eyedropper1'; } _renderDocumentStats(); statsPopover.hidden = false; + statsPopover.style.zIndex = String(topPortalZ()); // The editor pane clips overflow, so a footer-anchored absolute // popover can disappear underneath the document. Float it against // the viewport and place it above the stats button. @@ -10693,6 +10695,13 @@ import { attachColorPicker } from './colorPicker.js?v=20260910eyedropper1'; ['h5', 'Heading 5', 'H5'], ['h6', 'Heading 6', 'H6'], ], + font: [ + ['fontname:Arial', 'Arial', 'Aa'], + ['fontname:Georgia', 'Georgia', 'Gg'], + ['fontname:Times New Roman', 'Times New Roman', 'Tt'], + ['fontname:Verdana', 'Verdana', 'Vv'], + ['fontname:Courier New', 'Courier New', 'Cc'], + ], code: [['code', 'Inline code', '`'], ['codeblock', 'Code block', '```']], list: [ ['ul', 'Bullet list', '•'], @@ -11112,6 +11121,7 @@ import { attachColorPicker } from './colorPicker.js?v=20260910eyedropper1'; '#md-toolbar-sep-after-ai-reply', '#doc-ai-writing-btn', '[data-dd="heading"]', + '[data-dd="font"]', '#md-toolbar-sep-after-heading', '[data-dd="textsize"]', ], diff --git a/static/js/researchSynapse.js b/static/js/researchSynapse.js index 27e1e2572..87e0099ae 100644 --- a/static/js/researchSynapse.js +++ b/static/js/researchSynapse.js @@ -38,7 +38,7 @@ export default function createResearchSynapse(container, opts = {}) { · 00:00 -
Color target
Highlight target
', 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); diff --git a/tests/test_document_rich_keyboard_shortcuts.py b/tests/test_document_rich_keyboard_shortcuts.py index ae2130c00..532a58b5c 100644 --- a/tests/test_document_rich_keyboard_shortcuts.py +++ b/tests/test_document_rich_keyboard_shortcuts.py @@ -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); diff --git a/tests/test_document_rich_slash_menu.py b/tests/test_document_rich_slash_menu.py index 3006b54fb..c7a2fafea 100644 --- a/tests/test_document_rich_slash_menu.py +++ b/tests/test_document_rich_slash_menu.py @@ -52,7 +52,9 @@ def test_slash_menu_filters_converts_blocks_inserts_tables_and_fits_mobile(): current_content: 'Opening paragraph
Toggle target
', 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: 'Paragraph
', 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: 'Stateful text
', 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); diff --git a/tests/test_document_toolbar_order.py b/tests/test_document_toolbar_order.py index 4abdafb8a..ddf6355ab 100644 --- a/tests/test_document_toolbar_order.py +++ b/tests/test_document_toolbar_order.py @@ -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: 'Writing tools
', 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 diff --git a/tests/test_escape_inner_layers.py b/tests/test_escape_inner_layers.py index a9915531b..e63d3fc6c 100644 --- a/tests/test_escape_inner_layers.py +++ b/tests/test_escape_inner_layers.py @@ -22,7 +22,9 @@ def test_rich_escape_closes_toolbar_then_selection_badge() -> None: id: 'escape-doc', title: 'Escape', language: 'richtext', current_content: 'Selected sentence for testing
', 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} diff --git a/tests/test_research_synapse_ui.py b/tests/test_research_synapse_ui.py index 2a337a359..fb27c1abd 100644 --- a/tests/test_research_synapse_ui.py +++ b/tests/test_research_synapse_ui.py @@ -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 'starting…' 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