diff --git a/tests/test_agent_thread_chevron_static.py b/tests/test_agent_thread_chevron_static.py index 62c0103f8..413506e23 100644 --- a/tests/test_agent_thread_chevron_static.py +++ b/tests/test_agent_thread_chevron_static.py @@ -1,4 +1,5 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] @@ -7,7 +8,7 @@ ROOT = Path(__file__).resolve().parents[1] def test_agent_thread_chevron_uses_css_shape_in_live_and_history_renderers(): live = (ROOT / "static/js/chat.js").read_text() history = (ROOT / "static/js/chatRenderer.js").read_text() - css = (ROOT / "static/style.css").read_text() + css = app_css() for src in (live, history): assert 'class="agent-thread-chevron" aria-hidden="true">' in src diff --git a/tests/test_agent_thread_dot_alignment_css.py b/tests/test_agent_thread_dot_alignment_css.py index 5c3558f7b..f0f394225 100644 --- a/tests/test_agent_thread_dot_alignment_css.py +++ b/tests/test_agent_thread_dot_alignment_css.py @@ -14,11 +14,10 @@ file identifies the breakpoint it belongs to. import re from pathlib import Path +from tests.helpers.stylesheets import app_css -CSS = (Path(__file__).resolve().parents[1] / "static" / "style.css").read_text( - encoding="utf-8" -) +CSS = app_css() THREAD = r"^[ \t]*\.agent-thread[ \t]*\{" RAIL = r"^[ \t]*\.agent-thread::before[ \t]*\{" diff --git a/tests/test_background_chat_completion_ui_static.py b/tests/test_background_chat_completion_ui_static.py index 3a7f50b51..a64f7fb47 100644 --- a/tests/test_background_chat_completion_ui_static.py +++ b/tests/test_background_chat_completion_ui_static.py @@ -1,10 +1,11 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] CHAT = (ROOT / "static/js/chat.js").read_text() SESSIONS = (ROOT / "static/js/sessions.js").read_text() -CSS = (ROOT / "static/style.css").read_text() +CSS = app_css() def test_queued_prompts_are_persisted_per_session_and_restored_on_return(): diff --git a/tests/test_calendar_event_contrast.py b/tests/test_calendar_event_contrast.py index 1558551a5..ac692e451 100644 --- a/tests/test_calendar_event_contrast.py +++ b/tests/test_calendar_event_contrast.py @@ -4,11 +4,12 @@ import subprocess from pathlib import Path import pytest +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] CALENDAR_JS = ROOT / "static" / "js" / "calendar.js" -STYLE_CSS = ROOT / "static" / "style.css" +STYLE_CSS_TEXT = app_css() UTILS_JS = ROOT / "static" / "js" / "calendar" / "utils.js" pytestmark = pytest.mark.skipif(not shutil.which("node"), reason="node binary not on PATH") @@ -65,7 +66,7 @@ def test_calendar_readable_text_color_keeps_light_text_for_dark_colors(): def test_calendar_event_surfaces_use_computed_foreground_variable(): calendar_js = CALENDAR_JS.read_text(encoding="utf-8") - style_css = STYLE_CSS.read_text(encoding="utf-8") + style_css = STYLE_CSS_TEXT utils_js = UTILS_JS.read_text(encoding="utf-8") assert "_calReadableTextColor" in utils_js diff --git a/tests/test_card_menu_style_contract.py b/tests/test_card_menu_style_contract.py index f32fb3950..f073a56d2 100644 --- a/tests/test_card_menu_style_contract.py +++ b/tests/test_card_menu_style_contract.py @@ -1,8 +1,9 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] -STYLE = (ROOT / "static/style.css").read_text(encoding="utf-8") +STYLE = app_css() LIBRARY = (ROOT / "static/js/documentLibrary.js").read_text(encoding="utf-8") diff --git a/tests/test_chat_mode_pill_static.py b/tests/test_chat_mode_pill_static.py index 6ef49150b..8cf375bcf 100644 --- a/tests/test_chat_mode_pill_static.py +++ b/tests/test_chat_mode_pill_static.py @@ -1,4 +1,5 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] @@ -7,7 +8,7 @@ ROOT = Path(__file__).resolve().parents[1] def test_user_mode_pill_is_rendered_and_live_updated(): renderer = (ROOT / "static/js/chatRenderer.js").read_text(encoding="utf-8") chat = (ROOT / "static/js/chat.js").read_text(encoding="utf-8") - styles = (ROOT / "static/style.css").read_text(encoding="utf-8") + styles = app_css() routes = (ROOT / "routes/chat_routes.py").read_text(encoding="utf-8") helpers = (ROOT / "routes/chat_helpers.py").read_text(encoding="utf-8") diff --git a/tests/test_chatgpt_reasoning_and_grouping.py b/tests/test_chatgpt_reasoning_and_grouping.py index 9c36ad474..ebdbbc0fa 100644 --- a/tests/test_chatgpt_reasoning_and_grouping.py +++ b/tests/test_chatgpt_reasoning_and_grouping.py @@ -12,6 +12,7 @@ import subprocess import pytest from src import chatgpt_subscription, llm_core +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).parents[1] @@ -101,7 +102,7 @@ def test_model_picker_source_invariants(): def test_composer_reasoning_effort_ui_markup(): """Verify static/index.html and static/style.css include reasoning effort controls.""" html = (ROOT / "static/index.html").read_text(encoding="utf-8") - css = (ROOT / "static/style.css").read_text(encoding="utf-8") + css = app_css() # HTML elements assert 'id="reasoning-effort-wrap"' in html assert 'id="reasoning-effort-btn"' in html diff --git a/tests/test_chatgpt_subscription_usage_js.py b/tests/test_chatgpt_subscription_usage_js.py index 401cef6de..6014a85e9 100644 --- a/tests/test_chatgpt_subscription_usage_js.py +++ b/tests/test_chatgpt_subscription_usage_js.py @@ -6,11 +6,12 @@ import subprocess from pathlib import Path import pytest +from tests.helpers.stylesheets import app_css _REPO = Path(__file__).resolve().parent.parent _MODULE = _REPO / "static" / "js" / "chatgptSubscriptionUsage.js" _ADMIN = (_REPO / "static" / "js" / "admin.js").read_text(encoding="utf-8") -_STYLE = (_REPO / "static" / "style.css").read_text(encoding="utf-8") +_STYLE = app_css() pytestmark = pytest.mark.skipif(not shutil.which("node"), reason="node not on PATH") @@ -269,7 +270,7 @@ def test_refresh_and_reconnect_handlers_target_only_the_clicked_account(): def test_admin_renders_chatgpt_usage_collapsible_and_styled(): admin_source = (_REPO / "static" / "js" / "admin.js").read_text(encoding="utf-8") - style_source = (_REPO / "static" / "style.css").read_text(encoding="utf-8") + style_source = app_css() load_block = admin_source[admin_source.index("async function loadEndpoints()"):admin_source.index("function initEndpointForm()")] assert "adm-chatgpt-controls" in load_block assert "adm-chatgpt-usage-toggle" in load_block diff --git a/tests/test_compare_ask_user_routing.py b/tests/test_compare_ask_user_routing.py index 6fb61cecc..e39ab5b78 100644 --- a/tests/test_compare_ask_user_routing.py +++ b/tests/test_compare_ask_user_routing.py @@ -1,5 +1,6 @@ from pathlib import Path import re +from tests.helpers.stylesheets import app_css def test_compare_renders_ask_user_in_the_originating_pane(): @@ -80,7 +81,7 @@ def test_compare_pane_templates_hide_response_actions_until_response_exists(): root = Path(__file__).resolve().parents[1] index = (root / "static/js/compare/index.js").read_text(encoding="utf-8") panes = (root / "static/js/compare/panes.js").read_text(encoding="utf-8") - styles = (root / "static/style.css").read_text(encoding="utf-8") + styles = app_css() assert re.search(r"from './panes\.js\?v=[A-Za-z0-9_-]+'", index) assert re.search(r"from './selector\.js\?v=[A-Za-z0-9_-]+'", index) @@ -152,7 +153,7 @@ def test_compare_panes_have_visible_runtime_state_without_revealing_empty_action root = Path(__file__).resolve().parents[1] stream = (root / "static/js/compare/stream.js").read_text(encoding="utf-8") panes = (root / "static/js/compare/panes.js").read_text(encoding="utf-8") - styles = (root / "static/style.css").read_text(encoding="utf-8") + styles = app_css() assert "_paneEl.classList.remove('is-done', 'is-failed', 'is-awaiting-input');" in stream assert "_paneEl.classList.add('is-streaming');" in stream @@ -179,7 +180,7 @@ def test_compare_panes_surface_compact_result_summary(): index = (root / "static/js/compare/index.js").read_text(encoding="utf-8") panes = (root / "static/js/compare/panes.js").read_text(encoding="utf-8") stream = (root / "static/js/compare/stream.js").read_text(encoding="utf-8") - styles = (root / "static/style.css").read_text(encoding="utf-8") + styles = app_css() assert 'pane-header-row pane-header-secondary' in index assert 'class=\"pane-summary\" id=\"cmp-summary-' in index @@ -203,7 +204,7 @@ def test_compare_panes_surface_compact_result_summary(): 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") + styles = app_css() assert "function _selectionKey(sel)" in selector assert "function _duplicateSelectionKeys()" in selector diff --git a/tests/test_compare_mobile_tabs_static.py b/tests/test_compare_mobile_tabs_static.py index 14270050b..55ad7ab5a 100644 --- a/tests/test_compare_mobile_tabs_static.py +++ b/tests/test_compare_mobile_tabs_static.py @@ -1,4 +1,5 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parent.parent @@ -32,7 +33,7 @@ def test_mobile_compare_reconciles_tabs_after_pane_lifecycle_changes(): def test_mobile_compare_css_shows_only_the_active_card(): - css = _read("static/style.css") + css = app_css() mobile = css[css.index("/* Compare uses one full-width response card on phones."):] assert ".compare-mobile-tabs" in mobile @@ -44,7 +45,7 @@ def test_mobile_compare_css_shows_only_the_active_card(): def test_probe_feedback_and_actions_use_the_shared_card_layout(): selector = _read("static/js/compare/selector.js") - css = _read("static/style.css") + css = app_css() assert "probeFeedback.className = 'compare-probe-feedback'" in selector assert "probeFeedback.appendChild(detail)" in selector @@ -66,7 +67,7 @@ def test_probe_feedback_and_actions_use_the_shared_card_layout(): def test_probe_swap_reopens_and_highlights_the_failed_model_slot(): selector = _read("static/js/compare/selector.js") - css = _read("static/style.css") + css = app_css() assert "function _expandModelSlot(slotIdx)" in selector assert "row.dataset.slotIndex = String(idx);" in selector diff --git a/tests/test_compare_shuffle_visual_static.py b/tests/test_compare_shuffle_visual_static.py index 1e2f00505..70768c70a 100644 --- a/tests/test_compare_shuffle_visual_static.py +++ b/tests/test_compare_shuffle_visual_static.py @@ -1,5 +1,6 @@ from pathlib import Path import re +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parent.parent @@ -11,7 +12,7 @@ def _read(path: str) -> str: def test_compare_shuffle_shows_center_notice_with_dice_icon(): panes = _read("static/js/compare/panes.js") - css = _read("static/style.css") + css = app_css() assert "ICON_DICE" in panes assert "compare-shuffle-notice" in panes @@ -23,7 +24,7 @@ def test_compare_shuffle_shows_center_notice_with_dice_icon(): def test_compare_chat_and_agent_panes_expose_per_pane_inference_settings(): index = _read("static/js/compare/index.js") panes = _read("static/js/compare/panes.js") - css = _read("static/style.css") + css = app_css() assert "pane-settings-btn" in panes assert "paneSettingsButtonHtml" in index @@ -41,7 +42,7 @@ def test_compare_chat_and_agent_panes_expose_per_pane_inference_settings(): def test_compare_probe_control_has_requested_vertical_alignment(): index = _read("static/js/compare/index.js") probe = _read("static/js/compare/probe.js") - css = _read("static/style.css") + css = app_css() assert 'class="compare-check-icon"' in index assert 'Probe' in index @@ -75,7 +76,7 @@ def test_compare_cache_key_bumped_for_shuffle_notice(): def test_compare_score_button_label_is_nudged_up(): vote = _read("static/js/compare/vote.js") - css = _read("static/style.css") + css = app_css() assert 'Score' in vote assert ".compare-score-label" in css diff --git a/tests/test_dialog_aria.py b/tests/test_dialog_aria.py index 3513f9b0d..5ce8ce427 100644 --- a/tests/test_dialog_aria.py +++ b/tests/test_dialog_aria.py @@ -10,6 +10,7 @@ of the other tests in this suite. """ import re from pathlib import Path +from tests.helpers.stylesheets import app_css _REPO = Path(__file__).resolve().parent.parent _INDEX = (_REPO / "static" / "index.html").read_text(encoding="utf-8") @@ -47,7 +48,7 @@ def test_styled_confirm_and_prompt_are_modal_dialogs(): def test_styled_confirm_cancel_or_close_label_is_shifted_without_moving_button(): - css = (_REPO / "static" / "style.css").read_text(encoding="utf-8") + css = app_css() assert "cancelLabel.textContent = cancelText;" in _UI assert "cancelBtn.appendChild(cancelLabel);" in _UI diff --git a/tests/test_doc_library_open_orphaned.py b/tests/test_doc_library_open_orphaned.py index 58e52d673..76e47b7cc 100644 --- a/tests/test_doc_library_open_orphaned.py +++ b/tests/test_doc_library_open_orphaned.py @@ -18,6 +18,7 @@ import re import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import stylesheet_link_tags SRC = Path(__file__).resolve().parent.parent / "static/js/documentLibrary.js" @@ -96,7 +97,7 @@ def test_mobile_explicit_load_restores_full_editor_from_bottom_dock(): 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('
'); + await page.setContent('__ODY_STYLESHEETS__
'); const state = await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&mobile-library-open-test=1'); mod.init('/api'); @@ -134,6 +135,7 @@ def test_mobile_explicit_load_restores_full_editor_from_bottom_dock(): console.log(JSON.stringify(state)); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=SRC.parents[1], diff --git a/tests/test_document_edit_reference_js.py b/tests/test_document_edit_reference_js.py index 243d23406..65b607598 100644 --- a/tests/test_document_edit_reference_js.py +++ b/tests/test_document_edit_reference_js.py @@ -1,12 +1,13 @@ """Regression guards for document-selection references in chat bubbles.""" from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] RENDERER = (ROOT / "static/js/chatRenderer.js").read_text(encoding="utf-8") DOCUMENT = (ROOT / "static/js/document.js").read_text(encoding="utf-8") -STYLE = (ROOT / "static/style.css").read_text(encoding="utf-8") +STYLE = app_css() INDEX = (ROOT / "static/index.html").read_text(encoding="utf-8") APP = (ROOT / "static/app.js").read_text(encoding="utf-8") diff --git a/tests/test_document_editor_scroll.py b/tests/test_document_editor_scroll.py index 89cbc7b81..1023cab2f 100644 --- a/tests/test_document_editor_scroll.py +++ b/tests/test_document_editor_scroll.py @@ -9,11 +9,12 @@ document.js is browser-coupled and not importable in pytest. """ from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] DOC_JS = (ROOT / "static/js/document.js").read_text(encoding="utf-8") -STYLE_CSS = (ROOT / "static/style.css").read_text(encoding="utf-8") +STYLE_CSS = app_css() def test_document_textarea_scrollbar_is_visible(): diff --git a/tests/test_document_history_controls.py b/tests/test_document_history_controls.py index bd4b297ac..ddffe6e69 100644 --- a/tests/test_document_history_controls.py +++ b/tests/test_document_history_controls.py @@ -3,6 +3,7 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import stylesheet_link_tags ROOT = Path(__file__).resolve().parents[1] @@ -24,7 +25,7 @@ def test_mobile_rich_text_history_state_and_document_switch(): 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('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&history-controls-test=1'); window.documentModuleForTest = mod; @@ -85,6 +86,7 @@ def test_mobile_rich_text_history_state_and_document_switch(): console.log(JSON.stringify({ initial, typed, undone, redone, switched, overflow })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_library_mobile_footer.py b/tests/test_document_library_mobile_footer.py index f5a9921ae..ad94861c9 100644 --- a/tests/test_document_library_mobile_footer.py +++ b/tests/test_document_library_mobile_footer.py @@ -3,11 +3,13 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import app_css +from tests.helpers.stylesheets import stylesheet_link_tags ROOT = Path(__file__).resolve().parents[1] SOURCE = (ROOT / "static/js/documentLibrary.js").read_text(encoding="utf-8") -STYLE = (ROOT / "static/style.css").read_text(encoding="utf-8") +STYLE = app_css() def test_mobile_footer_exposes_delete_open_and_more(): @@ -53,7 +55,7 @@ def test_mobile_open_in_new_chat_copies_to_materialized_session(): const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 390, height: 844 }, hasTouch: true }); await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`); - await page.setContent('
'); + await page.setContent('__ODY_STYLESHEETS__
'); const state = await page.evaluate(async () => { let currentSession = 'current-chat'; let createDirectCalls = 0; @@ -106,6 +108,7 @@ def test_mobile_open_in_new_chat_copies_to_materialized_session(): console.log(JSON.stringify(state)); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_outline.py b/tests/test_document_outline.py index 4ab98d680..63f4ebc90 100644 --- a/tests/test_document_outline.py +++ b/tests/test_document_outline.py @@ -3,11 +3,13 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import app_css +from tests.helpers.stylesheets import stylesheet_link_tags 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") +STYLE = app_css() def _run_node(script: str): @@ -41,7 +43,7 @@ def test_markdown_outline_parses_structure_and_ignores_fenced_code(): '#### Final `code` section', ].join('\n'); console.log(JSON.stringify(parseMarkdownOutline(source))); - """ + """.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) ) assert [(entry["level"], entry["text"]) for entry in data] == [ (1, "Overview"), @@ -71,7 +73,7 @@ def test_outline_jumps_in_markdown_and_rich_text_and_fits_mobile(): const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 900, height: 700 } }); await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentOutline.js`); - await page.setContent('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&outline-test=1'); mod.init('/api'); @@ -126,7 +128,7 @@ def test_outline_jumps_in_markdown_and_rich_text_and_fits_mobile(): console.log(JSON.stringify({ markdownLabels, selected, liveLabels, mobileBox, richLabels, richCaretHeading })); await browser.close(); - """ + """.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) ) assert data["markdownLabels"] == ["Intro", "Details", "End"] assert data["selected"] == "## Details" diff --git a/tests/test_document_preview_edit_button.py b/tests/test_document_preview_edit_button.py index 52340f96d..ffe44f386 100644 --- a/tests/test_document_preview_edit_button.py +++ b/tests/test_document_preview_edit_button.py @@ -1,11 +1,12 @@ """Regression guards for the Markdown preview hover-to-edit control.""" from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] DOC_JS = (ROOT / "static/js/document.js").read_text(encoding="utf-8") -STYLE_CSS = (ROOT / "static/style.css").read_text(encoding="utf-8") +STYLE_CSS = app_css() def test_preview_installs_hover_edit_button(): diff --git a/tests/test_document_rich_checklist_enter.py b/tests/test_document_rich_checklist_enter.py index b36bdb67b..92154adb0 100644 --- a/tests/test_document_rich_checklist_enter.py +++ b/tests/test_document_rich_checklist_enter.py @@ -3,6 +3,7 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import stylesheet_link_tags ROOT = Path(__file__).resolve().parents[1] @@ -26,7 +27,7 @@ def test_enter_creates_unchecked_task_and_empty_enter_exits_cleanly(): 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/documentOutline.js`); - await page.setContent('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&checklist-enter-test=1'); mod.init('/api'); @@ -78,6 +79,7 @@ def test_enter_creates_unchecked_task_and_empty_enter_exits_cleanly(): console.log(JSON.stringify({ afterFirstEnter, ...data })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_rich_color_reset_and_contrast.py b/tests/test_document_rich_color_reset_and_contrast.py index 64c237a7b..287bef986 100644 --- a/tests/test_document_rich_color_reset_and_contrast.py +++ b/tests/test_document_rich_color_reset_and_contrast.py @@ -3,11 +3,13 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import app_css +from tests.helpers.stylesheets import stylesheet_link_tags 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") +STYLE = app_css() def test_color_controls_have_theme_reset_and_split_palettes(): @@ -27,7 +29,7 @@ def test_rich_colors_follow_theme_and_undo_as_one_edit(): await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`); await page.setContent(` + __ODY_STYLESHEETS__
`); await page.evaluate(async () => { const mod = await import('/static/js/document.js?rich-color-test=' + Date.now()); @@ -80,6 +82,7 @@ def test_rich_colors_follow_theme_and_undo_as_one_edit(): console.log(JSON.stringify({ palette, highlighted, highlightUndone, defaultColor, changedThemeColor, colorUndone })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_rich_docx_export.py b/tests/test_document_rich_docx_export.py index f5b6d5c6b..61cc71ad2 100644 --- a/tests/test_document_rich_docx_export.py +++ b/tests/test_document_rich_docx_export.py @@ -5,6 +5,7 @@ import subprocess import tempfile import zipfile from pathlib import Path +from tests.helpers.stylesheets import stylesheet_link_tags ROOT = Path(__file__).resolve().parents[1] @@ -64,7 +65,7 @@ def test_browser_word_export_contains_native_rich_docx_ooxml(): contentType: 'image/png', body: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64'), }})); - await page.setContent('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => {{ const mod = await import('/static/js/document.js?v=20260831richtexttools91&docx-export-test=1'); mod.init('/api'); @@ -89,6 +90,7 @@ def test_browser_word_export_contains_native_rich_docx_ooxml(): }})); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, @@ -158,7 +160,7 @@ def test_browser_markdown_word_export_keeps_heading_and_inline_formatting(): acceptDownloads: true, }}); await page.goto(`${{process.env.ODYSSEUS_TEST_STATIC_ORIGIN}}/static/js/documentStats.js`); - await page.setContent('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => {{ const mod = await import('/static/js/document.js?v=20260831richtexttools91&markdown-docx-export-test=1'); mod.init('/api'); @@ -180,6 +182,7 @@ def test_browser_markdown_word_export_keeps_heading_and_inline_formatting(): console.log(JSON.stringify({{ failure: await download.failure() }})); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_rich_find_boundaries.py b/tests/test_document_rich_find_boundaries.py index c4e293ad8..ca2280455 100644 --- a/tests/test_document_rich_find_boundaries.py +++ b/tests/test_document_rich_find_boundaries.py @@ -3,6 +3,7 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import stylesheet_link_tags ROOT = Path(__file__).resolve().parents[1] @@ -25,7 +26,7 @@ def test_find_rejects_cross_block_matches_but_supports_inline_matches_and_replac const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 900, height: 700 } }); await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentOutline.js`); - await page.setContent('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&find-boundaries-test=1'); mod.init('/api'); @@ -73,6 +74,7 @@ def test_find_rejects_cross_block_matches_but_supports_inline_matches_and_replac console.log(JSON.stringify({ crossParagraph, crossBreak, crossInline, ...data })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_rich_font_color_controls.py b/tests/test_document_rich_font_color_controls.py index cfecf8699..fa8691c2f 100644 --- a/tests/test_document_rich_font_color_controls.py +++ b/tests/test_document_rich_font_color_controls.py @@ -6,6 +6,7 @@ import subprocess from pathlib import Path from tests.helpers.stylesheets import app_css +from tests.helpers.stylesheets import stylesheet_link_tags ROOT = Path(__file__).resolve().parents[1] @@ -94,7 +95,7 @@ def test_numeric_font_size_and_custom_colors_work_on_desktop_and_mobile(): async function exercise(viewport, suffix) { const page = await browser.newPage({ viewport }); await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`); - await page.setContent('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async suffix => { const mod = await import(`/static/js/document.js?v=20260831richtexttools91&font-color=${suffix}`); mod.init('/api'); @@ -190,6 +191,7 @@ def test_numeric_font_size_and_custom_colors_work_on_desktop_and_mobile(): console.log(JSON.stringify({ desktop, mobile })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_rich_heading_enter.py b/tests/test_document_rich_heading_enter.py index d2df9ca89..9a876ca9d 100644 --- a/tests/test_document_rich_heading_enter.py +++ b/tests/test_document_rich_heading_enter.py @@ -3,6 +3,7 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import stylesheet_link_tags ROOT = Path(__file__).resolve().parents[1] @@ -29,7 +30,7 @@ def test_mobile_heading_enter_exits_cleanly_and_is_one_step_undoable(): 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('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&heading-enter=1'); mod.init('/api'); @@ -73,6 +74,7 @@ def test_mobile_heading_enter_exits_cleanly_and_is_one_step_undoable(): console.log(JSON.stringify({ entered, undone, redone })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, @@ -152,6 +154,7 @@ def test_heading_enter_preserves_shift_middle_and_empty_heading_semantics(): console.log(JSON.stringify(state)); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_rich_image_caption.py b/tests/test_document_rich_image_caption.py index 7c573dd03..b7e04101e 100644 --- a/tests/test_document_rich_image_caption.py +++ b/tests/test_document_rich_image_caption.py @@ -5,6 +5,7 @@ import subprocess from pathlib import Path from tests.helpers.stylesheets import app_css +from tests.helpers.stylesheets import stylesheet_link_tags ROOT = Path(__file__).resolve().parents[1] @@ -36,7 +37,7 @@ def test_mobile_image_caption_survives_resize_history_and_empty_removal(): 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('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&image-caption=1'); mod.init('/api'); @@ -116,6 +117,7 @@ def test_mobile_image_caption_survives_resize_history_and_empty_removal(): console.log(JSON.stringify({ added, undone, redone, resized, removed, removalUndone, menuRect, overflow })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_rich_input_rules.py b/tests/test_document_rich_input_rules.py index 0e93edc35..cf714fe03 100644 --- a/tests/test_document_rich_input_rules.py +++ b/tests/test_document_rich_input_rules.py @@ -5,6 +5,7 @@ import subprocess from pathlib import Path from tests.helpers.stylesheets import app_css +from tests.helpers.stylesheets import stylesheet_link_tags ROOT = Path(__file__).resolve().parents[1] @@ -28,7 +29,7 @@ def test_typing_markers_converts_blocks_and_preserves_following_text(): const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 900, height: 700 } }); await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentOutline.js`); - await page.setContent('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&input-rules-test=1'); mod.init('/api'); @@ -101,6 +102,7 @@ def test_typing_markers_converts_blocks_and_preserves_following_text(): console.log(JSON.stringify(data)); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_rich_keyboard_shortcuts.py b/tests/test_document_rich_keyboard_shortcuts.py index 89a09ec27..570daa87e 100644 --- a/tests/test_document_rich_keyboard_shortcuts.py +++ b/tests/test_document_rich_keyboard_shortcuts.py @@ -3,6 +3,7 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import stylesheet_link_tags ROOT = Path(__file__).resolve().parents[1] @@ -16,7 +17,7 @@ def test_rich_document_shortcuts_work_at_desktop_and_mobile_widths(): async function exercise(viewport, suffix) { const page = await browser.newPage({ viewport }); await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`); - await page.setContent('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async suffix => { const mod = await import(`/static/js/document.js?v=20260831richtexttools91&keyboard-shortcuts=${suffix}`); mod.init('/api'); @@ -94,6 +95,7 @@ def test_rich_document_shortcuts_work_at_desktop_and_mobile_widths(): console.log(JSON.stringify({ desktop, mobile })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_rich_selection_toolbar.py b/tests/test_document_rich_selection_toolbar.py index 306d45cce..b13977268 100644 --- a/tests/test_document_rich_selection_toolbar.py +++ b/tests/test_document_rich_selection_toolbar.py @@ -3,11 +3,13 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import app_css +from tests.helpers.stylesheets import stylesheet_link_tags 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") +STYLE = app_css() def test_selection_toolbar_uses_shared_formatting_path_and_preserves_range(): @@ -26,7 +28,7 @@ def test_selection_toolbar_formats_and_stays_inside_desktop_and_mobile_viewports const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 900, height: 700 } }); await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentOutline.js`); - await page.setContent('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&selection-toolbar-test=1'); mod.init('/api'); @@ -102,6 +104,7 @@ def test_selection_toolbar_formats_and_stays_inside_desktop_and_mobile_viewports console.log(JSON.stringify({ activeBold, desktopGeometry, formattedHtml, selectedAfterFormat, dismissed, mobileGeometry })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_rich_slash_menu.py b/tests/test_document_rich_slash_menu.py index 7c191985d..1ade47972 100644 --- a/tests/test_document_rich_slash_menu.py +++ b/tests/test_document_rich_slash_menu.py @@ -3,11 +3,13 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import app_css +from tests.helpers.stylesheets import stylesheet_link_tags 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") +STYLE = app_css() def test_slash_menu_reuses_rich_text_actions_and_is_accessible(): @@ -41,7 +43,7 @@ def test_slash_menu_filters_converts_blocks_inserts_tables_and_fits_mobile(): const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 900, height: 700 } }); await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentOutline.js`); - await page.setContent('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&slash-menu-test=1'); mod.init('/api'); @@ -149,6 +151,7 @@ def test_slash_menu_filters_converts_blocks_inserts_tables_and_fits_mobile(): console.log(JSON.stringify({ filtered, heading, noHeadingQuery, tableFiltered, table, noTableQuery, mobile, homeOptions, homeLabel, escaped, slashRemains, cleanedAria })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_rich_smart_link_paste.py b/tests/test_document_rich_smart_link_paste.py index 8f8c675e7..51b844209 100644 --- a/tests/test_document_rich_smart_link_paste.py +++ b/tests/test_document_rich_smart_link_paste.py @@ -3,6 +3,7 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import stylesheet_link_tags ROOT = Path(__file__).resolve().parents[1] @@ -14,7 +15,7 @@ def test_rich_url_paste_links_selections_and_plain_urls_without_unsafe_autolinks const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 900, height: 700 } }); await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`); - await page.setContent('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&smart-link-paste=1'); mod.init('/api'); @@ -133,6 +134,7 @@ def test_rich_url_paste_links_selections_and_plain_urls_without_unsafe_autolinks console.log(JSON.stringify({ selectedLink, storedAfterPaste, afterUndo, afterRedoLinks, caretLink, plainResults, emailLink, crossBlock })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_rich_structure_tools.py b/tests/test_document_rich_structure_tools.py index e6fd58922..049e7972b 100644 --- a/tests/test_document_rich_structure_tools.py +++ b/tests/test_document_rich_structure_tools.py @@ -3,11 +3,13 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import app_css +from tests.helpers.stylesheets import stylesheet_link_tags 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") +STYLE = app_css() def test_heading_levels_and_page_break_are_exposed_everywhere(): @@ -44,7 +46,7 @@ def test_mobile_headings_page_break_history_and_persistence(): 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('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&structure-tools-test=1'); window.__structureToolsDocModule = mod; @@ -152,6 +154,7 @@ def test_mobile_headings_page_break_history_and_persistence(): })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_rich_table_cell_alignment.py b/tests/test_document_rich_table_cell_alignment.py index a410723e3..8f07d4231 100644 --- a/tests/test_document_rich_table_cell_alignment.py +++ b/tests/test_document_rich_table_cell_alignment.py @@ -3,6 +3,7 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import stylesheet_link_tags ROOT = Path(__file__).resolve().parents[1] @@ -31,7 +32,7 @@ def test_mobile_table_cell_alignment_tracks_state_and_native_history(): 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('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&table-cell-alignment=1'); mod.init('/api'); @@ -122,6 +123,7 @@ def test_mobile_table_cell_alignment_tracks_state_and_native_history(): })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_rich_table_header_preservation.py b/tests/test_document_rich_table_header_preservation.py index 4c7c03626..f08825dd0 100644 --- a/tests/test_document_rich_table_header_preservation.py +++ b/tests/test_document_rich_table_header_preservation.py @@ -3,6 +3,7 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import stylesheet_link_tags ROOT = Path(__file__).resolve().parents[1] @@ -36,7 +37,7 @@ def test_mobile_structural_edits_preserve_header_modes_and_history(): 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('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&table-header-preservation-test=1'); mod.init('/api'); @@ -119,6 +120,7 @@ def test_mobile_structural_edits_preserve_header_modes_and_history(): })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_rich_table_headers.py b/tests/test_document_rich_table_headers.py index cd77ba93c..fb52d4630 100644 --- a/tests/test_document_rich_table_headers.py +++ b/tests/test_document_rich_table_headers.py @@ -3,6 +3,7 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import stylesheet_link_tags ROOT = Path(__file__).resolve().parents[1] @@ -40,7 +41,7 @@ def test_mobile_header_row_and_column_toggle_independently_with_undo(): 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('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&table-header-test=1'); mod.init('/api'); @@ -117,6 +118,7 @@ def test_mobile_header_row_and_column_toggle_independently_with_undo(): console.log(JSON.stringify({ initial, menuRect, columnOn, undone, redone, bothOn, rowOff, overflow })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_rich_table_merge_split.py b/tests/test_document_rich_table_merge_split.py index 85bb04387..6aeb76897 100644 --- a/tests/test_document_rich_table_merge_split.py +++ b/tests/test_document_rich_table_merge_split.py @@ -3,6 +3,7 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import stylesheet_link_tags ROOT = Path(__file__).resolve().parents[1] @@ -34,7 +35,7 @@ def test_mobile_merge_split_round_trip_preserves_headers_formatting_and_history( 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('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&table-merge-split=1'); mod.init('/api'); @@ -138,6 +139,7 @@ def test_mobile_merge_split_round_trip_preserves_headers_formatting_and_history( })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_rich_table_tab_history.py b/tests/test_document_rich_table_tab_history.py index abca1d02a..ec7de0013 100644 --- a/tests/test_document_rich_table_tab_history.py +++ b/tests/test_document_rich_table_tab_history.py @@ -3,6 +3,7 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import stylesheet_link_tags ROOT = Path(__file__).resolve().parents[1] @@ -31,7 +32,7 @@ def test_mobile_table_tab_navigation_row_creation_and_history(): 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('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&table-tab-history-test=1'); mod.init('/api'); @@ -98,6 +99,7 @@ def test_mobile_table_tab_navigation_row_creation_and_history(): console.log(JSON.stringify({ forward, backward, appended, undone, redone, overflow })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_rich_toolbar_menus.py b/tests/test_document_rich_toolbar_menus.py index 178820293..6aa33b379 100644 --- a/tests/test_document_rich_toolbar_menus.py +++ b/tests/test_document_rich_toolbar_menus.py @@ -3,11 +3,13 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import app_css +from tests.helpers.stylesheets import stylesheet_link_tags 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") +STYLE = app_css() def test_rich_toolbar_menus_expose_keyboard_and_context_state(): @@ -58,7 +60,7 @@ def test_mobile_toolbar_uses_native_momentum_and_distinct_activation_tokens(): const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 390, height: 844 }, hasTouch: true }); await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`); - await page.setContent('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&toggle-test=1'); mod.init('/api'); @@ -101,6 +103,7 @@ def test_mobile_toolbar_uses_native_momentum_and_distinct_activation_tokens(): console.log(JSON.stringify({ opened, closed, sliderOpened, sliderClosed, pickerOpened, colorClosed, before, scroll })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, @@ -127,7 +130,7 @@ def test_mobile_toolbar_menu_preserves_selection_and_restores_focus(): 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('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&toolbar-menu-test=1'); mod.init('/api'); @@ -204,6 +207,7 @@ def test_mobile_toolbar_menu_preserves_selection_and_restores_focus(): })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, @@ -242,7 +246,7 @@ def test_rich_toolbar_menus_track_live_formatting_values(): const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 900, height: 700 } }); await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`); - await page.setContent('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&toolbar-state-test=1'); mod.init('/api'); @@ -322,6 +326,7 @@ def test_rich_toolbar_menus_track_live_formatting_values(): })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_save_shortcut.py b/tests/test_document_save_shortcut.py index 0b1fe4f9b..c1265071e 100644 --- a/tests/test_document_save_shortcut.py +++ b/tests/test_document_save_shortcut.py @@ -3,6 +3,7 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import stylesheet_link_tags ROOT = Path(__file__).resolve().parents[1] @@ -38,7 +39,7 @@ def test_ctrl_s_saves_rich_text_immediately_once_and_updates_status(): }); }); await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`); - await page.setContent('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&save-shortcut-test=1'); mod.init('/api'); @@ -71,6 +72,7 @@ def test_ctrl_s_saves_rich_text_immediately_once_and_updates_status(): console.log(JSON.stringify({ dirty, saved, badge, requests })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_save_status.py b/tests/test_document_save_status.py index f29698ff0..487667799 100644 --- a/tests/test_document_save_status.py +++ b/tests/test_document_save_status.py @@ -3,6 +3,7 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import stylesheet_link_tags ROOT = Path(__file__).resolve().parents[1] @@ -32,7 +33,7 @@ def test_save_status_is_dirty_race_safe_and_reports_failures(): active -= 1; }); await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`); - await page.setContent('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=20260831richtexttools91&save-status-test=1'); mod.init('/api'); @@ -81,6 +82,7 @@ def test_save_status_is_dirty_race_safe_and_reports_failures(): console.log(JSON.stringify({ initial, dirty, duringSave, afterSave, afterFailure, bodies, maxActive, footer, mobile })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_document_suggestion_highlight.py b/tests/test_document_suggestion_highlight.py index 3dd819bda..f2272d66f 100644 --- a/tests/test_document_suggestion_highlight.py +++ b/tests/test_document_suggestion_highlight.py @@ -1,11 +1,12 @@ """Regression guards for exact inline document-suggestion highlighting.""" from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] DOC_JS = (ROOT / "static/js/document.js").read_text(encoding="utf-8") -STYLE_CSS = (ROOT / "static/style.css").read_text(encoding="utf-8") +STYLE_CSS = app_css() def test_suggestion_highlight_measures_exact_referenced_range(): diff --git a/tests/test_document_toolbar_arrow_offset.py b/tests/test_document_toolbar_arrow_offset.py index 5a145b620..096586249 100644 --- a/tests/test_document_toolbar_arrow_offset.py +++ b/tests/test_document_toolbar_arrow_offset.py @@ -1,4 +1,5 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] @@ -6,7 +7,7 @@ ROOT = Path(__file__).resolve().parents[1] def test_toolbar_arrows_have_real_flex_slots_outside_icon_scroller(): script = (ROOT / "static/js/document.js").read_text() - styles = (ROOT / "static/style.css").read_text() + styles = app_css() leading = script.index('class="md-toolbar-leading-controls"') left_arrow = script.index('id="md-scroll-left"') diff --git a/tests/test_document_toolbar_order.py b/tests/test_document_toolbar_order.py index 82e883380..669422830 100644 --- a/tests/test_document_toolbar_order.py +++ b/tests/test_document_toolbar_order.py @@ -3,6 +3,7 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import stylesheet_link_tags ROOT = Path(__file__).resolve().parents[1] @@ -38,7 +39,7 @@ def test_rich_toolbar_rendered_order_is_stable_on_desktop_and_mobile(): async function inspect(viewport, suffix) { const page = await browser.newPage({ viewport }); await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`); - await page.setContent('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async suffix => { const mod = await import(`/static/js/document.js?v=20260831richtexttools91&toolbar-order=${suffix}`); mod.init('/api'); @@ -79,6 +80,7 @@ def test_rich_toolbar_rendered_order_is_stable_on_desktop_and_mobile(): console.log(JSON.stringify({ desktop, mobile })); await browser.close(); """ + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ["node", "--input-type=module", "-e", script], cwd=ROOT, diff --git a/tests/test_editor_layer_thumbnail_render.py b/tests/test_editor_layer_thumbnail_render.py index aa9e283e1..286042011 100644 --- a/tests/test_editor_layer_thumbnail_render.py +++ b/tests/test_editor_layer_thumbnail_render.py @@ -1,4 +1,5 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] @@ -49,7 +50,7 @@ def test_group_rows_support_keyboard_selection(): def test_keyboard_focus_is_visible_for_layer_and_group_rows(): - styles = (ROOT / "static/style.css").read_text() + styles = app_css() assert ".ge-layer-item:focus-visible," in styles assert ".ge-layer-group-row:focus-visible" in styles diff --git a/tests/test_editor_quick_edit_static.py b/tests/test_editor_quick_edit_static.py index 7f90d92c4..72b17115a 100644 --- a/tests/test_editor_quick_edit_static.py +++ b/tests/test_editor_quick_edit_static.py @@ -1,9 +1,10 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] EDITOR = (ROOT / "static/js/galleryEditor.js").read_text(encoding="utf-8") -STYLE = (ROOT / "static/style.css").read_text(encoding="utf-8") +STYLE = app_css() def test_quick_edit_has_persistent_expanded_identity_and_labeled_input(): diff --git a/tests/test_editor_topbar_static.py b/tests/test_editor_topbar_static.py index 8f2ed3d50..377e47cf8 100644 --- a/tests/test_editor_topbar_static.py +++ b/tests/test_editor_topbar_static.py @@ -1,11 +1,12 @@ from pathlib import Path import re +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] TOPBAR = (ROOT / "static/js/editor/build/topbar.js").read_text(encoding="utf-8") OVERFLOW = (ROOT / "static/js/editor/wire-topbar-overflow.js").read_text(encoding="utf-8") -STYLE = (ROOT / "static/style.css").read_text(encoding="utf-8") +STYLE = app_css() def test_primary_editor_text_actions_use_stacked_toolbar_contract(): diff --git a/tests/test_email_folder_chip_static.py b/tests/test_email_folder_chip_static.py index 193cfa6aa..4410414ab 100644 --- a/tests/test_email_folder_chip_static.py +++ b/tests/test_email_folder_chip_static.py @@ -1,4 +1,5 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] @@ -6,7 +7,7 @@ ROOT = Path(__file__).resolve().parents[1] def test_folder_chip_stays_with_date_and_moves_down(): source = (ROOT / "static" / "js" / "emailLibrary.js").read_text() - css = (ROOT / "static" / "style.css").read_text() + css = app_css() assert 'class="email-meta-date-group"' in source group_markup = source[source.index('class="email-meta-date-group"'):][:180] diff --git a/tests/test_email_split_border_css.py b/tests/test_email_split_border_css.py index cf34d51b9..78a11899c 100644 --- a/tests/test_email_split_border_css.py +++ b/tests/test_email_split_border_css.py @@ -1,7 +1,8 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css -CSS = (Path(__file__).parents[1] / "static" / "style.css").read_text(encoding="utf-8") +CSS = app_css() def _rule(selector: str) -> str: diff --git a/tests/test_endpoint_card_ui_polish.py b/tests/test_endpoint_card_ui_polish.py index afd160b25..6f31e2a06 100644 --- a/tests/test_endpoint_card_ui_polish.py +++ b/tests/test_endpoint_card_ui_polish.py @@ -5,11 +5,12 @@ import shutil import subprocess from pathlib import Path import pytest +from tests.helpers.stylesheets import app_css _REPO = Path(__file__).resolve().parent.parent _ADMIN_JS = _REPO / "static" / "js" / "admin.js" _ADMIN = _ADMIN_JS.read_text(encoding="utf-8") -_STYLE = (_REPO / "static" / "style.css").read_text(encoding="utf-8") +_STYLE = app_css() pytestmark = pytest.mark.skipif(not shutil.which("node"), reason="node not on PATH") diff --git a/tests/test_escape_inner_layers.py b/tests/test_escape_inner_layers.py index f7818178d..d29dd5bb5 100644 --- a/tests/test_escape_inner_layers.py +++ b/tests/test_escape_inner_layers.py @@ -3,6 +3,7 @@ import json import subprocess from pathlib import Path +from tests.helpers.stylesheets import stylesheet_link_tags ROOT = Path(__file__).resolve().parents[1] @@ -14,7 +15,7 @@ def test_rich_escape_closes_toolbar_then_selection_badge() -> None: const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 900, height: 700 } }); await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`); - await page.setContent('
'); + await page.setContent('__ODY_STYLESHEETS__
'); await page.evaluate(async () => { const mod = await import('/static/js/document.js?v=escape-regression-1'); mod.init('/api'); @@ -55,6 +56,7 @@ def test_rich_escape_closes_toolbar_then_selection_badge() -> None: console.log(JSON.stringify({ before, afterOne, afterTwo })); await browser.close(); ''' + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ['node', '--input-type=module', '-e', script], cwd=ROOT, @@ -114,6 +116,7 @@ def test_email_escape_closes_inner_states_without_closing_library() -> None: console.log(JSON.stringify({ settings, select, reading })); await browser.close(); ''' + script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags()) result = subprocess.run( ['node', '--input-type=module', '-e', script], cwd=ROOT, diff --git a/tests/test_image_research_settings_static.py b/tests/test_image_research_settings_static.py index 1e07626e1..f56126469 100644 --- a/tests/test_image_research_settings_static.py +++ b/tests/test_image_research_settings_static.py @@ -1,4 +1,5 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] @@ -15,7 +16,7 @@ def test_image_settings_only_list_online_served_image_models(): def test_deep_research_fields_are_constrained_to_their_card(): - styles = (ROOT / "static/style.css").read_text(encoding="utf-8") + styles = app_css() rule = styles[styles.index("/* Deep Research uses long labels"):] assert ".admin-card:has(#set-researchSearch) .settings-row { min-width: 0; }" in rule diff --git a/tests/test_interrupted_resume_label_static.py b/tests/test_interrupted_resume_label_static.py index d4fddf388..b56119da6 100644 --- a/tests/test_interrupted_resume_label_static.py +++ b/tests/test_interrupted_resume_label_static.py @@ -1,11 +1,12 @@ from pathlib import Path import re +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] CHAT = (ROOT / "static/js/chat.js").read_text() RENDERER = (ROOT / "static/js/chatRenderer.js").read_text() -STYLE = (ROOT / "static/style.css").read_text() +STYLE = app_css() APP = (ROOT / "static/app.js").read_text() INDEX = (ROOT / "static/index.html").read_text() diff --git a/tests/test_mobile_search_position_static.py b/tests/test_mobile_search_position_static.py index 411244286..eb7d31bfb 100644 --- a/tests/test_mobile_search_position_static.py +++ b/tests/test_mobile_search_position_static.py @@ -1,4 +1,5 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] @@ -6,7 +7,7 @@ ROOT = Path(__file__).resolve().parents[1] def test_search_popup_freezes_offset_before_mobile_keyboard_focus() -> None: source = (ROOT / "static/js/search-chat.js").read_text(encoding="utf-8") - css = (ROOT / "static/style.css").read_text(encoding="utf-8") + css = app_css() set_offset = source.index("--search-overlay-top") show_overlay = source.index("overlay.classList.remove('hidden')", set_offset) diff --git a/tests/test_mobile_welcome_keyboard_static.py b/tests/test_mobile_welcome_keyboard_static.py index c957724c8..9339f1563 100644 --- a/tests/test_mobile_welcome_keyboard_static.py +++ b/tests/test_mobile_welcome_keyboard_static.py @@ -1,11 +1,12 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] def test_mobile_keyboard_fades_welcome_without_moving_it() -> None: - css = (ROOT / "static/style.css").read_text(encoding="utf-8") + css = app_css() init_js = (ROOT / "static/js/init.js").read_text(encoding="utf-8") kb_rule = css.split("#welcome-screen.kb-hidden {", 1)[1].split("}", 1)[0] @@ -22,7 +23,7 @@ def test_mobile_keyboard_fades_welcome_without_moving_it() -> None: def test_nobody_label_collapses_by_width_not_keyboard_height() -> None: - css = (ROOT / "static/style.css").read_text(encoding="utf-8") + css = app_css() label_rule = css.rsplit(".incognito-btn .incognito-label {", 1)[0] media_header = label_rule.rsplit("@media", 1)[1].split("{", 1)[0] diff --git a/tests/test_modal_dock_composer_clearance.py b/tests/test_modal_dock_composer_clearance.py index 5dfcfe2c1..b9480e06e 100644 --- a/tests/test_modal_dock_composer_clearance.py +++ b/tests/test_modal_dock_composer_clearance.py @@ -1,7 +1,8 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css -CSS = Path("static/style.css").read_text(encoding="utf-8") +CSS = app_css() INIT_JS = Path("static/js/init.js").read_text(encoding="utf-8") diff --git a/tests/test_notes_sidebar_new_note.py b/tests/test_notes_sidebar_new_note.py index 6caf9f9a3..7c78818be 100644 --- a/tests/test_notes_sidebar_new_note.py +++ b/tests/test_notes_sidebar_new_note.py @@ -1,4 +1,5 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] @@ -6,7 +7,7 @@ ROOT = Path(__file__).resolve().parents[1] def test_notes_sidebar_new_action_matches_library_structure_and_animation(): html = (ROOT / "static/index.html").read_text(encoding="utf-8") - css = (ROOT / "static/style.css").read_text(encoding="utf-8") + css = app_css() assert 'class="list-item-plus-btn sidebar-new-item-btn" id="notes-new-note-btn"' in html assert 'new' in html[html.index('id="notes-new-note-btn"'):] @@ -51,7 +52,7 @@ def test_checklist_preview_starts_at_first_unfinished_item(): def test_checklist_preview_releases_scroll_at_its_boundaries(): notes = (ROOT / "static/js/notes.js").read_text(encoding="utf-8") - css = (ROOT / "static/style.css").read_text(encoding="utf-8") + css = app_css() assert "el.addEventListener('wheel'" in notes assert "el.closest('.notes-pane-body')" in notes @@ -60,7 +61,7 @@ def test_checklist_preview_releases_scroll_at_its_boundaries(): def test_notes_tags_toggle_is_nudged_down(): - css = (ROOT / "static/style.css").read_text(encoding="utf-8") + css = app_css() rule = css[css.index(".notes-search-bar .notes-label-toggle {"):] rule = rule[:rule.index("}")] @@ -68,7 +69,7 @@ def test_notes_tags_toggle_is_nudged_down(): def test_notes_body_and_tag_arrow_are_nudged_down(): - css = (ROOT / "static/style.css").read_text(encoding="utf-8") + css = app_css() assert css.count("padding: 10px 8px 8px") >= 2 assert ".notes-pane-body .doclib-chip-scroll-arrow.right" in css @@ -76,7 +77,7 @@ def test_notes_body_and_tag_arrow_are_nudged_down(): def test_notes_tag_strip_cannot_grow_into_blank_space(): - css = (ROOT / "static/style.css").read_text(encoding="utf-8") + css = app_css() rule = css[css.index(".notes-pane-body > .doclib-chip-scroll-frame:has(> .notes-labels-bar) {"):] rule = rule[:rule.index("}")] diff --git a/tests/test_portal_dropdown_z_js.py b/tests/test_portal_dropdown_z_js.py index 71248ee7c..2e15549e9 100644 --- a/tests/test_portal_dropdown_z_js.py +++ b/tests/test_portal_dropdown_z_js.py @@ -16,6 +16,7 @@ import textwrap from pathlib import Path import pytest +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] @@ -100,9 +101,11 @@ def test_late_routed_dropdowns_use_top_portal_z(rel): assert "topPortalZ()" in src, f"{rel} must call topPortalZ() for its dropdown z" -@pytest.mark.parametrize("rel", ["static/js/tasks.js", "static/js/skills.js", "static/style.css"]) +@pytest.mark.parametrize("rel", ["static/js/tasks.js", "static/js/skills.js", "app-css"]) def test_no_hardcoded_portal_z_literals_remain(rel): - src = (ROOT / rel).read_text() + # "app-css" is the whole cascade: a moved rule must not escape this check + # by landing in a stylesheet this list does not name. + src = app_css() if rel == "app-css" else (ROOT / rel).read_text() # Match the exact 100000/100002 these dropdowns used; the trailing-digit # guard avoids false-matching an unrelated 1000000 elsewhere. hits = re.findall(r"z-index:\s*10000[02](?!\d)", src) diff --git a/tests/test_research_panel_ui.py b/tests/test_research_panel_ui.py index 304b273a7..ecec6db8a 100644 --- a/tests/test_research_panel_ui.py +++ b/tests/test_research_panel_ui.py @@ -1,4 +1,5 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] @@ -6,7 +7,7 @@ ROOT = Path(__file__).resolve().parents[1] def test_research_settings_use_custom_pickers_and_shared_provider_icons(): panel = (ROOT / "static/js/research/panel.js").read_text(encoding="utf-8") - style = (ROOT / "static/style.css").read_text(encoding="utf-8") + style = app_css() settings = (ROOT / "static/js/settings.js").read_text(encoding="utf-8") icons = (ROOT / "static/js/searchProviderIcons.js").read_text(encoding="utf-8") diff --git a/tests/test_resend_message_nondestructive.py b/tests/test_resend_message_nondestructive.py index ebd9bf435..a5274cc17 100644 --- a/tests/test_resend_message_nondestructive.py +++ b/tests/test_resend_message_nondestructive.py @@ -9,6 +9,7 @@ from pathlib import Path from types import SimpleNamespace from routes.history.history_routes import _keep_count_before_message +from tests.helpers.stylesheets import app_css _REPO = Path(__file__).resolve().parent.parent @@ -76,7 +77,7 @@ def test_footer_resend_uses_default_replacement_behavior(): def test_footer_resend_uses_round_svg_icon_not_text_glyph(): renderer = _CHAT_RENDERER_JS.read_text(encoding="utf-8") - style = (_REPO / "static" / "style.css").read_text(encoding="utf-8") + style = app_css() assert "const RESEND_ICON =" in renderer assert "resend-message-icon" in renderer diff --git a/tests/test_select_dropdown_theme_css.py b/tests/test_select_dropdown_theme_css.py index bcfdf23ec..80a59df8c 100644 --- a/tests/test_select_dropdown_theme_css.py +++ b/tests/test_select_dropdown_theme_css.py @@ -1,11 +1,10 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css -STYLE_CSS = Path(__file__).resolve().parents[1] / "static" / "style.css" - - +STYLE_CSS_TEXT = app_css() def _style_text() -> str: - return STYLE_CSS.read_text(encoding="utf-8") + return STYLE_CSS_TEXT def test_native_select_options_use_theme_tokens(): diff --git a/tests/test_selection_overlay_clear_static.py b/tests/test_selection_overlay_clear_static.py index d3671fc67..4761f4ff3 100644 --- a/tests/test_selection_overlay_clear_static.py +++ b/tests/test_selection_overlay_clear_static.py @@ -1,4 +1,5 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] @@ -6,7 +7,7 @@ ROOT = Path(__file__).resolve().parents[1] def test_selection_overlays_have_individual_clear_controls(): js = (ROOT / "static/js/document.js").read_text(encoding="utf-8") - css = (ROOT / "static/style.css").read_text(encoding="utf-8") + css = app_css() assert "function clearSelectionAt(index)" in js assert "className = 'doc-selection-overlay-clear'" in js diff --git a/tests/test_settings_shell_js_behavior.py b/tests/test_settings_shell_js_behavior.py index e353972a6..d1bb57635 100644 --- a/tests/test_settings_shell_js_behavior.py +++ b/tests/test_settings_shell_js_behavior.py @@ -14,6 +14,7 @@ import subprocess from pathlib import Path import pytest +from tests.helpers.stylesheets import app_css _REPO = Path(__file__).resolve().parent.parent @@ -22,11 +23,9 @@ _COORDINATOR_HELPER = ( _REPO / "tests" / "helpers" / "test_settings_shell_coordinator.mjs" ) _HAS_NODE = shutil.which("node") is not None -_STYLE = _REPO / "static" / "style.css" - - +_STYLE_TEXT = app_css() def test_settings_desktop_width_targets_settings_not_cookbook(): - source = _STYLE.read_text(encoding="utf-8") + source = _STYLE_TEXT settings_rule = re.search( r"(?ms)^\.settings-modal-content\s*\{[^}]*" diff --git a/tests/test_shared_tag_strips_static.py b/tests/test_shared_tag_strips_static.py index ba79b71ae..f58ee3b1b 100644 --- a/tests/test_shared_tag_strips_static.py +++ b/tests/test_shared_tag_strips_static.py @@ -1,8 +1,9 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] -STYLE = (ROOT / "static/style.css").read_text(encoding="utf-8") +STYLE = app_css() INDEX = (ROOT / "static/index.html").read_text(encoding="utf-8") CHIP_SCROLL = (ROOT / "static/js/chipScroll.js").read_text(encoding="utf-8") diff --git a/tests/test_sidebar_chat_list_sizing_static.py b/tests/test_sidebar_chat_list_sizing_static.py index aa923ba2c..488da68b9 100644 --- a/tests/test_sidebar_chat_list_sizing_static.py +++ b/tests/test_sidebar_chat_list_sizing_static.py @@ -1,11 +1,12 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parent.parent def test_sidebar_chat_list_uses_content_height_when_collapsed(): - css = (ROOT / "static/style.css").read_text(encoding="utf-8") + css = app_css() desktop = css.split("#sessions-section #session-list {", 1)[1].split("}", 1)[0] mobile = css.split("#sessions-section #session-list {", 2)[2].split("}", 1)[0] diff --git a/tests/test_skill_deeplink_static.py b/tests/test_skill_deeplink_static.py index b391c6677..d7985c3b1 100644 --- a/tests/test_skill_deeplink_static.py +++ b/tests/test_skill_deeplink_static.py @@ -1,5 +1,6 @@ from pathlib import Path import re +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] @@ -102,7 +103,7 @@ def test_markdown_flattens_legacy_notes_more_details(): def test_markdown_expands_note_and_skill_more_links_without_details(): markdown_src = (ROOT / "static/js/markdown.js").read_text() - style_src = (ROOT / "static/style.css").read_text() + style_src = app_css() assert "function extractMoreListPayloads" in markdown_src assert "ody-more-(notes|skills|memories|events|sessions)" in markdown_src @@ -119,7 +120,7 @@ def test_markdown_expands_note_and_skill_more_links_without_details(): def test_terminal_skill_listing_uses_clickable_bounded_formatter(): agent_src = (ROOT / "src/agent_loop.py").read_text() - style_src = (ROOT / "static/style.css").read_text() + style_src = app_css() assert "_qwen_skills_terminal_summary = _skills_list_summary_from_tool_output(" in agent_src assert '.msg-ai .body a[href^="#skill-"]' in style_src @@ -127,7 +128,7 @@ def test_terminal_skill_listing_uses_clickable_bounded_formatter(): def test_list_links_share_compact_typography(): - style_src = (ROOT / "static/style.css").read_text() + style_src = app_css() assert 'a.chat-link[href^="#events-more-"]' in style_src assert "font-size: 11px;" in style_src @@ -145,7 +146,7 @@ def test_open_skills_also_persists_skill_list(): def test_memory_deeplink_scrolls_and_flashes_even_when_filtered(): memory_src = (ROOT / "static/js/memory.js").read_text() - style_src = (ROOT / "static/style.css").read_text() + style_src = app_css() assert "function _resetMemoryDeepLinkFilters()" in memory_src assert "activeCategory = 'all';" in memory_src @@ -158,7 +159,7 @@ def test_memory_deeplink_scrolls_and_flashes_even_when_filtered(): def test_skill_and_memory_links_use_polished_chat_link_style(): - style_src = (ROOT / "static/style.css").read_text() + style_src = app_css() assert '.msg-ai .body a[href^="#skill-"]' in style_src assert '.msg-ai .body a[href^="#memory-"]' in style_src diff --git a/tests/test_stream_completion_scroll_stability.py b/tests/test_stream_completion_scroll_stability.py index 752c107be..dc360a774 100644 --- a/tests/test_stream_completion_scroll_stability.py +++ b/tests/test_stream_completion_scroll_stability.py @@ -1,11 +1,12 @@ from pathlib import Path import re +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] CHAT = (ROOT / "static/js/chat.js").read_text(encoding="utf-8") UI = (ROOT / "static/js/ui.js").read_text(encoding="utf-8") -STYLE = (ROOT / "static/style.css").read_text(encoding="utf-8") +STYLE = app_css() def test_terminal_and_canonical_renders_preserve_chat_scroll_anchor(): diff --git a/tests/test_stylesheet_test_hygiene.py b/tests/test_stylesheet_test_hygiene.py new file mode 100644 index 000000000..550a6c59a --- /dev/null +++ b/tests/test_stylesheet_test_hygiene.py @@ -0,0 +1,54 @@ +"""Tests must reason about the whole cascade, not one file of it. + +``static/style.css`` is being decomposed. A test that reads that file alone, +or builds a synthetic page linking only that file, silently loses every rule +that has moved: it keeps passing while covering less. Both mistakes existed +and are cheap to detect, so this fails on either. +""" + +import re +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SELF = Path(__file__).name + +# The helper module and the manifest/snapshot tests are about the stylesheet +# set itself, so naming the file is the point rather than a mistake. +ALLOWED = {SELF, "test_static_stylesheet_manifest.py", "test_css_computed_style_snapshot.py"} + +_DIRECT_READ = re.compile(r'["\']static/style\.css["\']|"static"\s*/\s*"style\.css"') +_LONE_LINK = re.compile(r']*href="/static/style\.css') + + +def _test_sources(): + return [p for p in sorted((ROOT / "tests").glob("*.py")) if p.name not in ALLOWED] + + +def test_sources_are_discoverable() -> None: + """Guard the guard: a layout change must not make this vacuous.""" + assert len(_test_sources()) > 100 + + +def test_no_test_reads_style_css_as_the_whole_cascade() -> None: + offenders = [ + p.name for p in _test_sources() + if _DIRECT_READ.search(p.read_text(encoding="utf-8")) + ] + + assert offenders == [], ( + "read the cascade with tests.helpers.stylesheets.app_css() instead of " + f"static/style.css alone: {offenders}" + ) + + +def test_no_synthetic_page_links_style_css_alone() -> None: + offenders = [ + p.name for p in _test_sources() + if _LONE_LINK.search(p.read_text(encoding="utf-8")) + ] + + assert offenders == [], ( + "build synthetic pages with tests.helpers.stylesheets.stylesheet_link_tags() " + f"so they get every stylesheet index.html loads: {offenders}" + ) diff --git a/tests/test_theme_sidebar_mobile.py b/tests/test_theme_sidebar_mobile.py index 9830adcb9..fa0aa0c5e 100644 --- a/tests/test_theme_sidebar_mobile.py +++ b/tests/test_theme_sidebar_mobile.py @@ -1,11 +1,10 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css -STYLE = Path(__file__).resolve().parents[1] / "static" / "style.css" - - +STYLE_TEXT = app_css() def test_mobile_sidebar_uses_the_sidebar_theme_surface(): - css = STYLE.read_text() + css = STYLE_TEXT mobile_drawer = css[css.index("/* Sidebar overlays chat on mobile */"):css.index("/* Backdrop behind sidebar */")] assert "background: var(--sidebar-bg, var(--panel)) !important;" in mobile_drawer assert "background: var(--panel) !important;" not in mobile_drawer diff --git a/tests/test_toast_ui_polish.py b/tests/test_toast_ui_polish.py index ef6ff1c39..c94528d6d 100644 --- a/tests/test_toast_ui_polish.py +++ b/tests/test_toast_ui_polish.py @@ -1,6 +1,7 @@ """Regression coverage for transient toast polish and accurate welcome tips.""" from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parent.parent @@ -9,7 +10,7 @@ ROOT = Path(__file__).resolve().parent.parent def test_notification_history_does_not_clutter_navigation_or_capture_toasts(): html = (ROOT / "static/index.html").read_text(encoding="utf-8") ui = (ROOT / "static/js/ui.js").read_text(encoding="utf-8") - css = (ROOT / "static/style.css").read_text(encoding="utf-8") + css = app_css() assert 'id="rail-notifications"' not in html assert 'id="sidebar-notifications-btn"' not in html @@ -32,7 +33,7 @@ def test_action_hint_is_part_of_action_button_and_close_is_grouped_beside_it(): def test_welcome_tips_are_plain_and_brief(): html = (ROOT / "static/index.html").read_text(encoding="utf-8") - css = (ROOT / "static/style.css").read_text(encoding="utf-8") + css = app_css() assert "Tip: Ctrl+K searches chats." in html assert "Tip: Ctrl+Alt+B toggles the sidebar." in html diff --git a/tests/test_tool_approval_frontend_routing.py b/tests/test_tool_approval_frontend_routing.py index 8b2f66d9a..3983b6330 100644 --- a/tests/test_tool_approval_frontend_routing.py +++ b/tests/test_tool_approval_frontend_routing.py @@ -1,5 +1,6 @@ from pathlib import Path import re +from tests.helpers.stylesheets import app_css def test_tool_approval_bypasses_polymorphic_send_button_actions(): @@ -22,7 +23,7 @@ def test_tool_approval_bypasses_polymorphic_send_button_actions(): def test_ask_user_card_has_no_close_button_and_chat_scale_text(): root = Path(__file__).resolve().parents[1] renderer = (root / "static/js/chatRenderer.js").read_text(encoding="utf-8") - styles = (root / "static/style.css").read_text(encoding="utf-8") + styles = app_css() assert "closeBtn.className = 'modal-close ask-user-close';" not in renderer assert "closeBtn.setAttribute('aria-label', 'Dismiss question');" not in renderer @@ -64,7 +65,7 @@ def test_ask_user_card_has_no_close_button_and_chat_scale_text(): def test_scroll_bottom_button_uses_dropdown_caret_glyph(): root = Path(__file__).resolve().parents[1] html = (root / "static/index.html").read_text(encoding="utf-8") - styles = (root / "static/style.css").read_text(encoding="utf-8") + styles = app_css() assert 'class="scroll-nav-caret"' in html assert "▾" in html diff --git a/tests/test_tool_header_icons_static.py b/tests/test_tool_header_icons_static.py index 5479ea368..e9e6d2e87 100644 --- a/tests/test_tool_header_icons_static.py +++ b/tests/test_tool_header_icons_static.py @@ -1,4 +1,5 @@ from pathlib import Path +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] @@ -7,7 +8,7 @@ ROOT = Path(__file__).resolve().parents[1] def test_tool_header_has_semantic_icons_for_live_and_saved_calls(): chat = (ROOT / "static/js/chat.js").read_text(encoding="utf-8") renderer = (ROOT / "static/js/chatRenderer.js").read_text(encoding="utf-8") - css = (ROOT / "static/style.css").read_text(encoding="utf-8") + css = app_css() assert "manage_calendar" in renderer assert "manage_memory" in renderer diff --git a/tests/test_visual_research_mode.py b/tests/test_visual_research_mode.py index bdcd6da37..53c175c3c 100644 --- a/tests/test_visual_research_mode.py +++ b/tests/test_visual_research_mode.py @@ -5,6 +5,7 @@ import pytest from src.deep_research import CATEGORY_PROMPTS, _infer_research_category from src.tool_schemas import FUNCTION_TOOL_SCHEMAS from src.visual_report import _standard_visual_variant +from tests.helpers.stylesheets import app_css ROOT = Path(__file__).resolve().parents[1] @@ -20,7 +21,7 @@ def _tool_schema(name: str) -> dict: def test_visual_research_mode_is_removed_from_ui_prompt_and_tool_schema(): panel_source = (ROOT / "static/js/research/panel.js").read_text(encoding="utf-8") - style_source = (ROOT / "static/style.css").read_text(encoding="utf-8") + style_source = app_css() routes_source = (ROOT / "routes/research/research_routes.py").read_text(encoding="utf-8") category_schema = _tool_schema("trigger_research")["parameters"]["properties"]["category"] rounds_schema = _tool_schema("trigger_research")["parameters"]["properties"]["max_rounds"]