Merge pull request #19 from o3LL/refactor/tests-read-the-whole-cascade

refactor(tests): read the whole cascade instead of style.css alone
This commit is contained in:
Alexandre Teixeira
2026-09-30 19:27:58 +01:00
committed by GitHub
72 changed files with 273 additions and 116 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] 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(): def test_agent_thread_chevron_uses_css_shape_in_live_and_history_renderers():
live = (ROOT / "static/js/chat.js").read_text() live = (ROOT / "static/js/chat.js").read_text()
history = (ROOT / "static/js/chatRenderer.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): for src in (live, history):
assert 'class="agent-thread-chevron" aria-hidden="true"></span>' in src assert 'class="agent-thread-chevron" aria-hidden="true"></span>' in src
+2 -3
View File
@@ -14,11 +14,10 @@ file identifies the breakpoint it belongs to.
import re import re
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
CSS = (Path(__file__).resolve().parents[1] / "static" / "style.css").read_text( CSS = app_css()
encoding="utf-8"
)
THREAD = r"^[ \t]*\.agent-thread[ \t]*\{" THREAD = r"^[ \t]*\.agent-thread[ \t]*\{"
RAIL = r"^[ \t]*\.agent-thread::before[ \t]*\{" RAIL = r"^[ \t]*\.agent-thread::before[ \t]*\{"
@@ -1,10 +1,11 @@
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
CHAT = (ROOT / "static/js/chat.js").read_text() CHAT = (ROOT / "static/js/chat.js").read_text()
SESSIONS = (ROOT / "static/js/sessions.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(): def test_queued_prompts_are_persisted_per_session_and_restored_on_return():
+3 -2
View File
@@ -4,11 +4,12 @@ import subprocess
from pathlib import Path from pathlib import Path
import pytest import pytest
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
CALENDAR_JS = ROOT / "static" / "js" / "calendar.js" 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" UTILS_JS = ROOT / "static" / "js" / "calendar" / "utils.js"
pytestmark = pytest.mark.skipif(not shutil.which("node"), reason="node binary not on PATH") 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(): def test_calendar_event_surfaces_use_computed_foreground_variable():
calendar_js = CALENDAR_JS.read_text(encoding="utf-8") 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") utils_js = UTILS_JS.read_text(encoding="utf-8")
assert "_calReadableTextColor" in utils_js assert "_calReadableTextColor" in utils_js
+2 -1
View File
@@ -1,8 +1,9 @@
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] 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") LIBRARY = (ROOT / "static/js/documentLibrary.js").read_text(encoding="utf-8")
+2 -1
View File
@@ -1,4 +1,5 @@
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] 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(): def test_user_mode_pill_is_rendered_and_live_updated():
renderer = (ROOT / "static/js/chatRenderer.js").read_text(encoding="utf-8") renderer = (ROOT / "static/js/chatRenderer.js").read_text(encoding="utf-8")
chat = (ROOT / "static/js/chat.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") routes = (ROOT / "routes/chat_routes.py").read_text(encoding="utf-8")
helpers = (ROOT / "routes/chat_helpers.py").read_text(encoding="utf-8") helpers = (ROOT / "routes/chat_helpers.py").read_text(encoding="utf-8")
+2 -1
View File
@@ -12,6 +12,7 @@ import subprocess
import pytest import pytest
from src import chatgpt_subscription, llm_core from src import chatgpt_subscription, llm_core
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).parents[1] ROOT = Path(__file__).parents[1]
@@ -101,7 +102,7 @@ def test_model_picker_source_invariants():
def test_composer_reasoning_effort_ui_markup(): def test_composer_reasoning_effort_ui_markup():
"""Verify static/index.html and static/style.css include reasoning effort controls.""" """Verify static/index.html and static/style.css include reasoning effort controls."""
html = (ROOT / "static/index.html").read_text(encoding="utf-8") 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 # HTML elements
assert 'id="reasoning-effort-wrap"' in html assert 'id="reasoning-effort-wrap"' in html
assert 'id="reasoning-effort-btn"' in html assert 'id="reasoning-effort-btn"' in html
+3 -2
View File
@@ -6,11 +6,12 @@ import subprocess
from pathlib import Path from pathlib import Path
import pytest import pytest
from tests.helpers.stylesheets import app_css
_REPO = Path(__file__).resolve().parent.parent _REPO = Path(__file__).resolve().parent.parent
_MODULE = _REPO / "static" / "js" / "chatgptSubscriptionUsage.js" _MODULE = _REPO / "static" / "js" / "chatgptSubscriptionUsage.js"
_ADMIN = (_REPO / "static" / "js" / "admin.js").read_text(encoding="utf-8") _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") 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(): def test_admin_renders_chatgpt_usage_collapsible_and_styled():
admin_source = (_REPO / "static" / "js" / "admin.js").read_text(encoding="utf-8") 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()")] 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-controls" in load_block
assert "adm-chatgpt-usage-toggle" in load_block assert "adm-chatgpt-usage-toggle" in load_block
+5 -4
View File
@@ -1,5 +1,6 @@
from pathlib import Path from pathlib import Path
import re import re
from tests.helpers.stylesheets import app_css
def test_compare_renders_ask_user_in_the_originating_pane(): 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] root = Path(__file__).resolve().parents[1]
index = (root / "static/js/compare/index.js").read_text(encoding="utf-8") index = (root / "static/js/compare/index.js").read_text(encoding="utf-8")
panes = (root / "static/js/compare/panes.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 './panes\.js\?v=[A-Za-z0-9_-]+'", index)
assert re.search(r"from './selector\.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] root = Path(__file__).resolve().parents[1]
stream = (root / "static/js/compare/stream.js").read_text(encoding="utf-8") stream = (root / "static/js/compare/stream.js").read_text(encoding="utf-8")
panes = (root / "static/js/compare/panes.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.remove('is-done', 'is-failed', 'is-awaiting-input');" in stream
assert "_paneEl.classList.add('is-streaming');" 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") index = (root / "static/js/compare/index.js").read_text(encoding="utf-8")
panes = (root / "static/js/compare/panes.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") 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 'pane-header-row pane-header-secondary' in index
assert 'class=\"pane-summary\" id=\"cmp-summary-' 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(): def test_compare_selector_surfaces_duplicate_warning_without_blocking_start():
root = Path(__file__).resolve().parents[1] root = Path(__file__).resolve().parents[1]
selector = (root / "static/js/compare/selector.js").read_text(encoding="utf-8") 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 _selectionKey(sel)" in selector
assert "function _duplicateSelectionKeys()" in selector assert "function _duplicateSelectionKeys()" in selector
+4 -3
View File
@@ -1,4 +1,5 @@
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parent.parent 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(): 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."):] mobile = css[css.index("/* Compare uses one full-width response card on phones."):]
assert ".compare-mobile-tabs" in mobile 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(): def test_probe_feedback_and_actions_use_the_shared_card_layout():
selector = _read("static/js/compare/selector.js") 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.className = 'compare-probe-feedback'" in selector
assert "probeFeedback.appendChild(detail)" 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(): def test_probe_swap_reopens_and_highlights_the_failed_model_slot():
selector = _read("static/js/compare/selector.js") selector = _read("static/js/compare/selector.js")
css = _read("static/style.css") css = app_css()
assert "function _expandModelSlot(slotIdx)" in selector assert "function _expandModelSlot(slotIdx)" in selector
assert "row.dataset.slotIndex = String(idx);" in selector assert "row.dataset.slotIndex = String(idx);" in selector
+5 -4
View File
@@ -1,5 +1,6 @@
from pathlib import Path from pathlib import Path
import re import re
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parent.parent 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(): def test_compare_shuffle_shows_center_notice_with_dice_icon():
panes = _read("static/js/compare/panes.js") panes = _read("static/js/compare/panes.js")
css = _read("static/style.css") css = app_css()
assert "ICON_DICE" in panes assert "ICON_DICE" in panes
assert "compare-shuffle-notice" 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(): def test_compare_chat_and_agent_panes_expose_per_pane_inference_settings():
index = _read("static/js/compare/index.js") index = _read("static/js/compare/index.js")
panes = _read("static/js/compare/panes.js") panes = _read("static/js/compare/panes.js")
css = _read("static/style.css") css = app_css()
assert "pane-settings-btn" in panes assert "pane-settings-btn" in panes
assert "paneSettingsButtonHtml" in index 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(): def test_compare_probe_control_has_requested_vertical_alignment():
index = _read("static/js/compare/index.js") index = _read("static/js/compare/index.js")
probe = _read("static/js/compare/probe.js") probe = _read("static/js/compare/probe.js")
css = _read("static/style.css") css = app_css()
assert 'class="compare-check-icon"' in index assert 'class="compare-check-icon"' in index
assert '<span class="compare-check-label">Probe</span>' in index assert '<span class="compare-check-label">Probe</span>' in index
@@ -75,7 +76,7 @@ def test_compare_cache_key_bumped_for_shuffle_notice():
def test_compare_score_button_label_is_nudged_up(): def test_compare_score_button_label_is_nudged_up():
vote = _read("static/js/compare/vote.js") vote = _read("static/js/compare/vote.js")
css = _read("static/style.css") css = app_css()
assert '<span class="compare-score-label">Score</span>' in vote assert '<span class="compare-score-label">Score</span>' in vote
assert ".compare-score-label" in css assert ".compare-score-label" in css
+2 -1
View File
@@ -10,6 +10,7 @@ of the other tests in this suite.
""" """
import re import re
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
_REPO = Path(__file__).resolve().parent.parent _REPO = Path(__file__).resolve().parent.parent
_INDEX = (_REPO / "static" / "index.html").read_text(encoding="utf-8") _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(): 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 "cancelLabel.textContent = cancelText;" in _UI
assert "cancelBtn.appendChild(cancelLabel);" in _UI assert "cancelBtn.appendChild(cancelLabel);" in _UI
+3 -1
View File
@@ -18,6 +18,7 @@ import re
import json import json
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import stylesheet_link_tags
SRC = Path(__file__).resolve().parent.parent / "static/js/documentLibrary.js" 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 browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 390, height: 844 } }); 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
const state = await page.evaluate(async () => { const state = await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&mobile-library-open-test=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&mobile-library-open-test=1');
mod.init('/api'); mod.init('/api');
@@ -134,6 +135,7 @@ def test_mobile_explicit_load_restores_full_editor_from_bottom_dock():
console.log(JSON.stringify(state)); console.log(JSON.stringify(state));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=SRC.parents[1], cwd=SRC.parents[1],
+2 -1
View File
@@ -1,12 +1,13 @@
"""Regression guards for document-selection references in chat bubbles.""" """Regression guards for document-selection references in chat bubbles."""
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
RENDERER = (ROOT / "static/js/chatRenderer.js").read_text(encoding="utf-8") RENDERER = (ROOT / "static/js/chatRenderer.js").read_text(encoding="utf-8")
DOCUMENT = (ROOT / "static/js/document.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") INDEX = (ROOT / "static/index.html").read_text(encoding="utf-8")
APP = (ROOT / "static/app.js").read_text(encoding="utf-8") APP = (ROOT / "static/app.js").read_text(encoding="utf-8")
+2 -1
View File
@@ -9,11 +9,12 @@ document.js is browser-coupled and not importable in pytest.
""" """
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
DOC_JS = (ROOT / "static/js/document.js").read_text(encoding="utf-8") 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(): def test_document_textarea_scrollbar_is_visible():
+3 -1
View File
@@ -3,6 +3,7 @@
import json import json
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import stylesheet_link_tags
ROOT = Path(__file__).resolve().parents[1] 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 browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 390, height: 844 } }); 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&history-controls-test=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&history-controls-test=1');
window.documentModuleForTest = mod; 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 })); console.log(JSON.stringify({ initial, typed, undone, redone, switched, overflow }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
+5 -2
View File
@@ -3,11 +3,13 @@
import json import json
import subprocess import subprocess
from pathlib import Path 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] ROOT = Path(__file__).resolve().parents[1]
SOURCE = (ROOT / "static/js/documentLibrary.js").read_text(encoding="utf-8") 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(): 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 browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 390, height: 844 }, hasTouch: 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
const state = await page.evaluate(async () => { const state = await page.evaluate(async () => {
let currentSession = 'current-chat'; let currentSession = 'current-chat';
let createDirectCalls = 0; let createDirectCalls = 0;
@@ -106,6 +108,7 @@ def test_mobile_open_in_new_chat_copies_to_materialized_session():
console.log(JSON.stringify(state)); console.log(JSON.stringify(state));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
+6 -4
View File
@@ -3,11 +3,13 @@
import json import json
import subprocess import subprocess
from pathlib import Path 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] ROOT = Path(__file__).resolve().parents[1]
DOC_JS = (ROOT / "static/js/document.js").read_text(encoding="utf-8") 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): def _run_node(script: str):
@@ -41,7 +43,7 @@ def test_markdown_outline_parses_structure_and_ignores_fenced_code():
'#### Final `code` section', '#### Final `code` section',
].join('\n'); ].join('\n');
console.log(JSON.stringify(parseMarkdownOutline(source))); console.log(JSON.stringify(parseMarkdownOutline(source)));
""" """.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
) )
assert [(entry["level"], entry["text"]) for entry in data] == [ assert [(entry["level"], entry["text"]) for entry in data] == [
(1, "Overview"), (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 browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 900, height: 700 } }); 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentOutline.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&outline-test=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&outline-test=1');
mod.init('/api'); 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 })); console.log(JSON.stringify({ markdownLabels, selected, liveLabels, mobileBox, richLabels, richCaretHeading }));
await browser.close(); await browser.close();
""" """.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
) )
assert data["markdownLabels"] == ["Intro", "Details", "End"] assert data["markdownLabels"] == ["Intro", "Details", "End"]
assert data["selected"] == "## Details" assert data["selected"] == "## Details"
+2 -1
View File
@@ -1,11 +1,12 @@
"""Regression guards for the Markdown preview hover-to-edit control.""" """Regression guards for the Markdown preview hover-to-edit control."""
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
DOC_JS = (ROOT / "static/js/document.js").read_text(encoding="utf-8") 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(): def test_preview_installs_hover_edit_button():
+3 -1
View File
@@ -3,6 +3,7 @@
import json import json
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import stylesheet_link_tags
ROOT = Path(__file__).resolve().parents[1] 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 browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 390, height: 844 } }); 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentOutline.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&checklist-enter-test=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&checklist-enter-test=1');
mod.init('/api'); mod.init('/api');
@@ -78,6 +79,7 @@ def test_enter_creates_unchecked_task_and_empty_enter_exits_cleanly():
console.log(JSON.stringify({ afterFirstEnter, ...data })); console.log(JSON.stringify({ afterFirstEnter, ...data }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
@@ -3,11 +3,13 @@
import json import json
import subprocess import subprocess
from pathlib import Path 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] ROOT = Path(__file__).resolve().parents[1]
DOC_JS = (ROOT / "static/js/document.js").read_text(encoding="utf-8") 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(): def test_color_controls_have_theme_reset_and_split_palettes():
@@ -33,7 +35,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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent(`<style> await page.setContent(`<style>
:root { --fg:#d8dee9; --bg:#17191f; --panel:#20232b; --border:#444; --red:#e45b6c; --accent-primary:#e45b6c; } :root { --fg:#d8dee9; --bg:#17191f; --panel:#20232b; --border:#444; --red:#e45b6c; --accent-primary:#e45b6c; }
</style><link rel="stylesheet" href="/static/style.css?rich-color-test=1"> </style>__ODY_STYLESHEETS__
<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>`); <div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>`);
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?rich-color-test=' + Date.now()); const mod = await import('/static/js/document.js?rich-color-test=' + Date.now());
@@ -86,6 +88,7 @@ def test_rich_colors_follow_theme_and_undo_as_one_edit():
console.log(JSON.stringify({ palette, highlighted, highlightUndone, defaultColor, changedThemeColor, colorUndone })); console.log(JSON.stringify({ palette, highlighted, highlightUndone, defaultColor, changedThemeColor, colorUndone }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
+5 -2
View File
@@ -5,6 +5,7 @@ import subprocess
import tempfile import tempfile
import zipfile import zipfile
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import stylesheet_link_tags
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
@@ -64,7 +65,7 @@ def test_browser_word_export_contains_native_rich_docx_ooxml():
contentType: 'image/png', contentType: 'image/png',
body: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64'), body: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64'),
}})); }}));
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => {{ await page.evaluate(async () => {{
const mod = await import('/static/js/document.js?v=20260831richtexttools91&docx-export-test=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&docx-export-test=1');
mod.init('/api'); mod.init('/api');
@@ -89,6 +90,7 @@ def test_browser_word_export_contains_native_rich_docx_ooxml():
}})); }}));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
@@ -158,7 +160,7 @@ def test_browser_markdown_word_export_keeps_heading_and_inline_formatting():
acceptDownloads: true, acceptDownloads: true,
}}); }});
await page.goto(`${{process.env.ODYSSEUS_TEST_STATIC_ORIGIN}}/static/js/documentStats.js`); await page.goto(`${{process.env.ODYSSEUS_TEST_STATIC_ORIGIN}}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => {{ await page.evaluate(async () => {{
const mod = await import('/static/js/document.js?v=20260831richtexttools91&markdown-docx-export-test=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&markdown-docx-export-test=1');
mod.init('/api'); 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() }})); console.log(JSON.stringify({{ failure: await download.failure() }}));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
+3 -1
View File
@@ -3,6 +3,7 @@
import json import json
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import stylesheet_link_tags
ROOT = Path(__file__).resolve().parents[1] 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 browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 900, height: 700 } }); 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentOutline.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&find-boundaries-test=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&find-boundaries-test=1');
mod.init('/api'); 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 })); console.log(JSON.stringify({ crossParagraph, crossBreak, crossInline, ...data }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
@@ -6,6 +6,7 @@ import subprocess
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css from tests.helpers.stylesheets import app_css
from tests.helpers.stylesheets import stylesheet_link_tags
ROOT = Path(__file__).resolve().parents[1] 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) { async function exercise(viewport, suffix) {
const page = await browser.newPage({ viewport }); const page = await browser.newPage({ viewport });
await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`); await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async suffix => { await page.evaluate(async suffix => {
const mod = await import(`/static/js/document.js?v=20260831richtexttools91&font-color=${suffix}`); const mod = await import(`/static/js/document.js?v=20260831richtexttools91&font-color=${suffix}`);
mod.init('/api'); 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 })); console.log(JSON.stringify({ desktop, mobile }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
+4 -1
View File
@@ -3,6 +3,7 @@
import json import json
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import stylesheet_link_tags
ROOT = Path(__file__).resolve().parents[1] 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 browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 390, height: 844 } }); 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&heading-enter=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&heading-enter=1');
mod.init('/api'); 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 })); console.log(JSON.stringify({ entered, undone, redone }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
@@ -152,6 +154,7 @@ def test_heading_enter_preserves_shift_middle_and_empty_heading_semantics():
console.log(JSON.stringify(state)); console.log(JSON.stringify(state));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
+3 -1
View File
@@ -5,6 +5,7 @@ import subprocess
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css from tests.helpers.stylesheets import app_css
from tests.helpers.stylesheets import stylesheet_link_tags
ROOT = Path(__file__).resolve().parents[1] 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 browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 390, height: 844 } }); 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&image-caption=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&image-caption=1');
mod.init('/api'); 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 })); console.log(JSON.stringify({ added, undone, redone, resized, removed, removalUndone, menuRect, overflow }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
+3 -1
View File
@@ -5,6 +5,7 @@ import subprocess
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css from tests.helpers.stylesheets import app_css
from tests.helpers.stylesheets import stylesheet_link_tags
ROOT = Path(__file__).resolve().parents[1] 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 browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 900, height: 700 } }); 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentOutline.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&input-rules-test=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&input-rules-test=1');
mod.init('/api'); mod.init('/api');
@@ -101,6 +102,7 @@ def test_typing_markers_converts_blocks_and_preserves_following_text():
console.log(JSON.stringify(data)); console.log(JSON.stringify(data));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
@@ -3,6 +3,7 @@
import json import json
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import stylesheet_link_tags
ROOT = Path(__file__).resolve().parents[1] 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) { async function exercise(viewport, suffix) {
const page = await browser.newPage({ viewport }); const page = await browser.newPage({ viewport });
await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`); await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async suffix => { await page.evaluate(async suffix => {
const mod = await import(`/static/js/document.js?v=20260831richtexttools91&keyboard-shortcuts=${suffix}`); const mod = await import(`/static/js/document.js?v=20260831richtexttools91&keyboard-shortcuts=${suffix}`);
mod.init('/api'); mod.init('/api');
@@ -94,6 +95,7 @@ def test_rich_document_shortcuts_work_at_desktop_and_mobile_widths():
console.log(JSON.stringify({ desktop, mobile })); console.log(JSON.stringify({ desktop, mobile }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
@@ -3,11 +3,13 @@
import json import json
import subprocess import subprocess
from pathlib import Path 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] ROOT = Path(__file__).resolve().parents[1]
DOC_JS = (ROOT / "static/js/document.js").read_text(encoding="utf-8") 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(): 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 browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 900, height: 700 } }); 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentOutline.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&selection-toolbar-test=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&selection-toolbar-test=1');
mod.init('/api'); 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 })); console.log(JSON.stringify({ activeBold, desktopGeometry, formattedHtml, selectedAfterFormat, dismissed, mobileGeometry }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
+5 -2
View File
@@ -3,11 +3,13 @@
import json import json
import subprocess import subprocess
from pathlib import Path 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] ROOT = Path(__file__).resolve().parents[1]
DOC_JS = (ROOT / "static/js/document.js").read_text(encoding="utf-8") 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(): 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 browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 900, height: 700 } }); 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentOutline.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&slash-menu-test=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&slash-menu-test=1');
mod.init('/api'); 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 })); console.log(JSON.stringify({ filtered, heading, noHeadingQuery, tableFiltered, table, noTableQuery, mobile, homeOptions, homeLabel, escaped, slashRemains, cleanedAria }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
+3 -1
View File
@@ -3,6 +3,7 @@
import json import json
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import stylesheet_link_tags
ROOT = Path(__file__).resolve().parents[1] 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 browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 900, height: 700 } }); 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&smart-link-paste=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&smart-link-paste=1');
mod.init('/api'); 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 })); console.log(JSON.stringify({ selectedLink, storedAfterPaste, afterUndo, afterRedoLinks, caretLink, plainResults, emailLink, crossBlock }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
+5 -2
View File
@@ -3,11 +3,13 @@
import json import json
import subprocess import subprocess
from pathlib import Path 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] ROOT = Path(__file__).resolve().parents[1]
DOC_JS = (ROOT / "static/js/document.js").read_text(encoding="utf-8") 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(): 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 browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 390, height: 844 } }); 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&structure-tools-test=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&structure-tools-test=1');
window.__structureToolsDocModule = mod; window.__structureToolsDocModule = mod;
@@ -152,6 +154,7 @@ def test_mobile_headings_page_break_history_and_persistence():
})); }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
@@ -3,6 +3,7 @@
import json import json
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import stylesheet_link_tags
ROOT = Path(__file__).resolve().parents[1] 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 browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 390, height: 844 } }); 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&table-cell-alignment=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&table-cell-alignment=1');
mod.init('/api'); mod.init('/api');
@@ -122,6 +123,7 @@ def test_mobile_table_cell_alignment_tracks_state_and_native_history():
})); }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
@@ -3,6 +3,7 @@
import json import json
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import stylesheet_link_tags
ROOT = Path(__file__).resolve().parents[1] 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 browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 390, height: 844 } }); 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&table-header-preservation-test=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&table-header-preservation-test=1');
mod.init('/api'); mod.init('/api');
@@ -119,6 +120,7 @@ def test_mobile_structural_edits_preserve_header_modes_and_history():
})); }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
+3 -1
View File
@@ -3,6 +3,7 @@
import json import json
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import stylesheet_link_tags
ROOT = Path(__file__).resolve().parents[1] 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 browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 390, height: 844 } }); 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&table-header-test=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&table-header-test=1');
mod.init('/api'); 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 })); console.log(JSON.stringify({ initial, menuRect, columnOn, undone, redone, bothOn, rowOff, overflow }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
@@ -3,6 +3,7 @@
import json import json
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import stylesheet_link_tags
ROOT = Path(__file__).resolve().parents[1] 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 browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 390, height: 844 } }); 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&table-merge-split=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&table-merge-split=1');
mod.init('/api'); mod.init('/api');
@@ -138,6 +139,7 @@ def test_mobile_merge_split_round_trip_preserves_headers_formatting_and_history(
})); }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
@@ -3,6 +3,7 @@
import json import json
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import stylesheet_link_tags
ROOT = Path(__file__).resolve().parents[1] 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 browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 390, height: 844 } }); 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&table-tab-history-test=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&table-tab-history-test=1');
mod.init('/api'); 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 })); console.log(JSON.stringify({ forward, backward, appended, undone, redone, overflow }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
+9 -4
View File
@@ -3,11 +3,13 @@
import json import json
import subprocess import subprocess
from pathlib import Path 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] ROOT = Path(__file__).resolve().parents[1]
DOC_JS = (ROOT / "static/js/document.js").read_text(encoding="utf-8") 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(): 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 browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 390, height: 844 }, hasTouch: 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&toggle-test=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&toggle-test=1');
mod.init('/api'); 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 })); console.log(JSON.stringify({ opened, closed, sliderOpened, sliderClosed, pickerOpened, colorClosed, before, scroll }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
@@ -127,7 +130,7 @@ def test_mobile_toolbar_menu_preserves_selection_and_restores_focus():
const browser = await chromium.launch({ headless: true }); const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 390, height: 844 } }); 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&toolbar-menu-test=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&toolbar-menu-test=1');
mod.init('/api'); mod.init('/api');
@@ -204,6 +207,7 @@ def test_mobile_toolbar_menu_preserves_selection_and_restores_focus():
})); }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
@@ -242,7 +246,7 @@ def test_rich_toolbar_menus_track_live_formatting_values():
const browser = await chromium.launch({ headless: true }); const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 900, height: 700 } }); 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&toolbar-state-test=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&toolbar-state-test=1');
mod.init('/api'); mod.init('/api');
@@ -322,6 +326,7 @@ def test_rich_toolbar_menus_track_live_formatting_values():
})); }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
+3 -1
View File
@@ -3,6 +3,7 @@
import json import json
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import stylesheet_link_tags
ROOT = Path(__file__).resolve().parents[1] 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&save-shortcut-test=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&save-shortcut-test=1');
mod.init('/api'); 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 })); console.log(JSON.stringify({ dirty, saved, badge, requests }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
+3 -1
View File
@@ -3,6 +3,7 @@
import json import json
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import stylesheet_link_tags
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
@@ -32,7 +33,7 @@ def test_save_status_is_dirty_race_safe_and_reports_failures():
active -= 1; active -= 1;
}); });
await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`); await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=20260831richtexttools91&save-status-test=1'); const mod = await import('/static/js/document.js?v=20260831richtexttools91&save-status-test=1');
mod.init('/api'); 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 })); console.log(JSON.stringify({ initial, dirty, duringSave, afterSave, afterFailure, bodies, maxActive, footer, mobile }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
+2 -1
View File
@@ -1,11 +1,12 @@
"""Regression guards for exact inline document-suggestion highlighting.""" """Regression guards for exact inline document-suggestion highlighting."""
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
DOC_JS = (ROOT / "static/js/document.js").read_text(encoding="utf-8") 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(): def test_suggestion_highlight_measures_exact_referenced_range():
+2 -1
View File
@@ -1,4 +1,5 @@
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] 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(): def test_toolbar_arrows_have_real_flex_slots_outside_icon_scroller():
script = (ROOT / "static/js/document.js").read_text() 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"') leading = script.index('class="md-toolbar-leading-controls"')
left_arrow = script.index('id="md-scroll-left"') left_arrow = script.index('id="md-scroll-left"')
+3 -1
View File
@@ -3,6 +3,7 @@
import json import json
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import stylesheet_link_tags
ROOT = Path(__file__).resolve().parents[1] 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) { async function inspect(viewport, suffix) {
const page = await browser.newPage({ viewport }); const page = await browser.newPage({ viewport });
await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`); await page.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css?v=20260831richtexttools91"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async suffix => { await page.evaluate(async suffix => {
const mod = await import(`/static/js/document.js?v=20260831richtexttools91&toolbar-order=${suffix}`); const mod = await import(`/static/js/document.js?v=20260831richtexttools91&toolbar-order=${suffix}`);
mod.init('/api'); 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 })); console.log(JSON.stringify({ desktop, mobile }));
await browser.close(); await browser.close();
""" """
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
["node", "--input-type=module", "-e", script], ["node", "--input-type=module", "-e", script],
cwd=ROOT, cwd=ROOT,
+2 -1
View File
@@ -1,4 +1,5 @@
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] 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(): 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-item:focus-visible," in styles
assert ".ge-layer-group-row:focus-visible" in styles assert ".ge-layer-group-row:focus-visible" in styles
+2 -1
View File
@@ -1,9 +1,10 @@
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
EDITOR = (ROOT / "static/js/galleryEditor.js").read_text(encoding="utf-8") 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(): def test_quick_edit_has_persistent_expanded_identity_and_labeled_input():
+2 -1
View File
@@ -1,11 +1,12 @@
from pathlib import Path from pathlib import Path
import re import re
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
TOPBAR = (ROOT / "static/js/editor/build/topbar.js").read_text(encoding="utf-8") 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") 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(): def test_primary_editor_text_actions_use_stacked_toolbar_contract():
+2 -1
View File
@@ -1,4 +1,5 @@
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] 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(): def test_folder_chip_stays_with_date_and_moves_down():
source = (ROOT / "static" / "js" / "emailLibrary.js").read_text() 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 assert 'class="email-meta-date-group"' in source
group_markup = source[source.index('class="email-meta-date-group"'):][:180] group_markup = source[source.index('class="email-meta-date-group"'):][:180]
+2 -1
View File
@@ -1,7 +1,8 @@
from pathlib import Path 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: def _rule(selector: str) -> str:
+2 -1
View File
@@ -5,11 +5,12 @@ import shutil
import subprocess import subprocess
from pathlib import Path from pathlib import Path
import pytest import pytest
from tests.helpers.stylesheets import app_css
_REPO = Path(__file__).resolve().parent.parent _REPO = Path(__file__).resolve().parent.parent
_ADMIN_JS = _REPO / "static" / "js" / "admin.js" _ADMIN_JS = _REPO / "static" / "js" / "admin.js"
_ADMIN = _ADMIN_JS.read_text(encoding="utf-8") _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") pytestmark = pytest.mark.skipif(not shutil.which("node"), reason="node not on PATH")
+4 -1
View File
@@ -3,6 +3,7 @@
import json import json
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import stylesheet_link_tags
ROOT = Path(__file__).resolve().parents[1] 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 browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 900, height: 700 } }); 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.goto(`${process.env.ODYSSEUS_TEST_STATIC_ORIGIN}/static/js/documentStats.js`);
await page.setContent('<link rel="stylesheet" href="/static/style.css"><div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>'); await page.setContent('__ODY_STYLESHEETS__<div id="toast"></div><div id="chat-container"></div><div id="sidebar"></div>');
await page.evaluate(async () => { await page.evaluate(async () => {
const mod = await import('/static/js/document.js?v=escape-regression-1'); const mod = await import('/static/js/document.js?v=escape-regression-1');
mod.init('/api'); mod.init('/api');
@@ -55,6 +56,7 @@ def test_rich_escape_closes_toolbar_then_selection_badge() -> None:
console.log(JSON.stringify({ before, afterOne, afterTwo })); console.log(JSON.stringify({ before, afterOne, afterTwo }));
await browser.close(); await browser.close();
''' '''
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
['node', '--input-type=module', '-e', script], ['node', '--input-type=module', '-e', script],
cwd=ROOT, cwd=ROOT,
@@ -114,6 +116,7 @@ def test_email_escape_closes_inner_states_without_closing_library() -> None:
console.log(JSON.stringify({ settings, select, reading })); console.log(JSON.stringify({ settings, select, reading }));
await browser.close(); await browser.close();
''' '''
script = script.replace("__ODY_STYLESHEETS__", stylesheet_link_tags())
result = subprocess.run( result = subprocess.run(
['node', '--input-type=module', '-e', script], ['node', '--input-type=module', '-e', script],
cwd=ROOT, cwd=ROOT,
+2 -1
View File
@@ -1,4 +1,5 @@
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] 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(): 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"):] rule = styles[styles.index("/* Deep Research uses long labels"):]
assert ".admin-card:has(#set-researchSearch) .settings-row { min-width: 0; }" in rule assert ".admin-card:has(#set-researchSearch) .settings-row { min-width: 0; }" in rule
@@ -1,11 +1,12 @@
from pathlib import Path from pathlib import Path
import re import re
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
CHAT = (ROOT / "static/js/chat.js").read_text() CHAT = (ROOT / "static/js/chat.js").read_text()
RENDERER = (ROOT / "static/js/chatRenderer.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() APP = (ROOT / "static/app.js").read_text()
INDEX = (ROOT / "static/index.html").read_text() INDEX = (ROOT / "static/index.html").read_text()
+2 -1
View File
@@ -1,4 +1,5 @@
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] 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: def test_search_popup_freezes_offset_before_mobile_keyboard_focus() -> None:
source = (ROOT / "static/js/search-chat.js").read_text(encoding="utf-8") 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") set_offset = source.index("--search-overlay-top")
show_overlay = source.index("overlay.classList.remove('hidden')", set_offset) show_overlay = source.index("overlay.classList.remove('hidden')", set_offset)
+3 -2
View File
@@ -1,11 +1,12 @@
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
def test_mobile_keyboard_fades_welcome_without_moving_it() -> None: 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") 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] 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: 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] label_rule = css.rsplit(".incognito-btn .incognito-label {", 1)[0]
media_header = label_rule.rsplit("@media", 1)[1].split("{", 1)[0] media_header = label_rule.rsplit("@media", 1)[1].split("{", 1)[0]
+2 -1
View File
@@ -1,7 +1,8 @@
from pathlib import Path 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") INIT_JS = Path("static/js/init.js").read_text(encoding="utf-8")
+6 -5
View File
@@ -1,4 +1,5 @@
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] 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(): def test_notes_sidebar_new_action_matches_library_structure_and_animation():
html = (ROOT / "static/index.html").read_text(encoding="utf-8") 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 'class="list-item-plus-btn sidebar-new-item-btn" id="notes-new-note-btn"' in html
assert '<span class="list-item-plus-label">new</span>' in html[html.index('id="notes-new-note-btn"'):] assert '<span class="list-item-plus-label">new</span>' 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(): def test_checklist_preview_releases_scroll_at_its_boundaries():
notes = (ROOT / "static/js/notes.js").read_text(encoding="utf-8") 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.addEventListener('wheel'" in notes
assert "el.closest('.notes-pane-body')" 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(): 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 = css[css.index(".notes-search-bar .notes-label-toggle {"):]
rule = rule[:rule.index("}")] 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(): 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 css.count("padding: 10px 8px 8px") >= 2
assert ".notes-pane-body .doclib-chip-scroll-arrow.right" in css 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(): 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 = css[css.index(".notes-pane-body > .doclib-chip-scroll-frame:has(> .notes-labels-bar) {"):]
rule = rule[:rule.index("}")] rule = rule[:rule.index("}")]
+5 -2
View File
@@ -16,6 +16,7 @@ import textwrap
from pathlib import Path from pathlib import Path
import pytest import pytest
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] 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" 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): 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 # Match the exact 100000/100002 these dropdowns used; the trailing-digit
# guard avoids false-matching an unrelated 1000000 elsewhere. # guard avoids false-matching an unrelated 1000000 elsewhere.
hits = re.findall(r"z-index:\s*10000[02](?!\d)", src) hits = re.findall(r"z-index:\s*10000[02](?!\d)", src)
+2 -1
View File
@@ -1,4 +1,5 @@
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] 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(): def test_research_settings_use_custom_pickers_and_shared_provider_icons():
panel = (ROOT / "static/js/research/panel.js").read_text(encoding="utf-8") 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") settings = (ROOT / "static/js/settings.js").read_text(encoding="utf-8")
icons = (ROOT / "static/js/searchProviderIcons.js").read_text(encoding="utf-8") icons = (ROOT / "static/js/searchProviderIcons.js").read_text(encoding="utf-8")
+2 -1
View File
@@ -9,6 +9,7 @@ from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
from routes.history.history_routes import _keep_count_before_message from routes.history.history_routes import _keep_count_before_message
from tests.helpers.stylesheets import app_css
_REPO = Path(__file__).resolve().parent.parent _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(): def test_footer_resend_uses_round_svg_icon_not_text_glyph():
renderer = _CHAT_RENDERER_JS.read_text(encoding="utf-8") 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 "const RESEND_ICON =" in renderer
assert "resend-message-icon" in renderer assert "resend-message-icon" in renderer
+3 -4
View File
@@ -1,11 +1,10 @@
from pathlib import Path 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: def _style_text() -> str:
return STYLE_CSS.read_text(encoding="utf-8") return STYLE_CSS_TEXT
def test_native_select_options_use_theme_tokens(): def test_native_select_options_use_theme_tokens():
+2 -1
View File
@@ -1,4 +1,5 @@
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
@@ -6,7 +7,7 @@ ROOT = Path(__file__).resolve().parents[1]
def test_selection_overlays_have_individual_clear_controls(): def test_selection_overlays_have_individual_clear_controls():
js = (ROOT / "static/js/document.js").read_text(encoding="utf-8") 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 "function clearSelectionAt(index)" in js
assert "className = 'doc-selection-overlay-clear'" in js assert "className = 'doc-selection-overlay-clear'" in js
+3 -4
View File
@@ -14,6 +14,7 @@ import subprocess
from pathlib import Path from pathlib import Path
import pytest import pytest
from tests.helpers.stylesheets import app_css
_REPO = Path(__file__).resolve().parent.parent _REPO = Path(__file__).resolve().parent.parent
@@ -22,11 +23,9 @@ _COORDINATOR_HELPER = (
_REPO / "tests" / "helpers" / "test_settings_shell_coordinator.mjs" _REPO / "tests" / "helpers" / "test_settings_shell_coordinator.mjs"
) )
_HAS_NODE = shutil.which("node") is not None _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(): def test_settings_desktop_width_targets_settings_not_cookbook():
source = _STYLE.read_text(encoding="utf-8") source = _STYLE_TEXT
settings_rule = re.search( settings_rule = re.search(
r"(?ms)^\.settings-modal-content\s*\{[^}]*" r"(?ms)^\.settings-modal-content\s*\{[^}]*"
+2 -1
View File
@@ -1,8 +1,9 @@
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] 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") INDEX = (ROOT / "static/index.html").read_text(encoding="utf-8")
CHIP_SCROLL = (ROOT / "static/js/chipScroll.js").read_text(encoding="utf-8") CHIP_SCROLL = (ROOT / "static/js/chipScroll.js").read_text(encoding="utf-8")
@@ -1,11 +1,12 @@
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
def test_sidebar_chat_list_uses_content_height_when_collapsed(): 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] desktop = css.split("#sessions-section #session-list {", 1)[1].split("}", 1)[0]
mobile = css.split("#sessions-section #session-list {", 2)[2].split("}", 1)[0] mobile = css.split("#sessions-section #session-list {", 2)[2].split("}", 1)[0]
+6 -5
View File
@@ -1,5 +1,6 @@
from pathlib import Path from pathlib import Path
import re import re
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] 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(): def test_markdown_expands_note_and_skill_more_links_without_details():
markdown_src = (ROOT / "static/js/markdown.js").read_text() 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 "function extractMoreListPayloads" in markdown_src
assert "ody-more-(notes|skills|memories|events|sessions)" 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(): def test_terminal_skill_listing_uses_clickable_bounded_formatter():
agent_src = (ROOT / "src/agent_loop.py").read_text() 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 "_qwen_skills_terminal_summary = _skills_list_summary_from_tool_output(" in agent_src
assert '.msg-ai .body a[href^="#skill-"]' in style_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(): 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 'a.chat-link[href^="#events-more-"]' in style_src
assert "font-size: 11px;" 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(): def test_memory_deeplink_scrolls_and_flashes_even_when_filtered():
memory_src = (ROOT / "static/js/memory.js").read_text() 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 "function _resetMemoryDeepLinkFilters()" in memory_src
assert "activeCategory = 'all';" 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(): 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^="#skill-"]' in style_src
assert '.msg-ai .body a[href^="#memory-"]' in style_src assert '.msg-ai .body a[href^="#memory-"]' in style_src
@@ -1,11 +1,12 @@
from pathlib import Path from pathlib import Path
import re import re
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
CHAT = (ROOT / "static/js/chat.js").read_text(encoding="utf-8") CHAT = (ROOT / "static/js/chat.js").read_text(encoding="utf-8")
UI = (ROOT / "static/js/ui.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(): def test_terminal_and_canonical_renders_preserve_chat_scroll_anchor():
+54
View File
@@ -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'<link[^>]*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}"
)
+3 -4
View File
@@ -1,11 +1,10 @@
from pathlib import Path 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(): 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 */")] 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(--sidebar-bg, var(--panel)) !important;" in mobile_drawer
assert "background: var(--panel) !important;" not in mobile_drawer assert "background: var(--panel) !important;" not in mobile_drawer
+3 -2
View File
@@ -1,6 +1,7 @@
"""Regression coverage for transient toast polish and accurate welcome tips.""" """Regression coverage for transient toast polish and accurate welcome tips."""
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parent.parent 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(): def test_notification_history_does_not_clutter_navigation_or_capture_toasts():
html = (ROOT / "static/index.html").read_text(encoding="utf-8") html = (ROOT / "static/index.html").read_text(encoding="utf-8")
ui = (ROOT / "static/js/ui.js").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="rail-notifications"' not in html
assert 'id="sidebar-notifications-btn"' 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(): def test_welcome_tips_are_plain_and_brief():
html = (ROOT / "static/index.html").read_text(encoding="utf-8") 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+K searches chats." in html
assert "Tip: Ctrl+Alt+B toggles the sidebar." in html assert "Tip: Ctrl+Alt+B toggles the sidebar." in html
+3 -2
View File
@@ -1,5 +1,6 @@
from pathlib import Path from pathlib import Path
import re import re
from tests.helpers.stylesheets import app_css
def test_tool_approval_bypasses_polymorphic_send_button_actions(): 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(): def test_ask_user_card_has_no_close_button_and_chat_scale_text():
root = Path(__file__).resolve().parents[1] root = Path(__file__).resolve().parents[1]
renderer = (root / "static/js/chatRenderer.js").read_text(encoding="utf-8") 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.className = 'modal-close ask-user-close';" not in renderer
assert "closeBtn.setAttribute('aria-label', 'Dismiss question');" 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(): def test_scroll_bottom_button_uses_dropdown_caret_glyph():
root = Path(__file__).resolve().parents[1] root = Path(__file__).resolve().parents[1]
html = (root / "static/index.html").read_text(encoding="utf-8") 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 'class="scroll-nav-caret"' in html
assert "&#9662;" in html assert "&#9662;" in html
+2 -1
View File
@@ -1,4 +1,5 @@
from pathlib import Path from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] 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(): def test_tool_header_has_semantic_icons_for_live_and_saved_calls():
chat = (ROOT / "static/js/chat.js").read_text(encoding="utf-8") chat = (ROOT / "static/js/chat.js").read_text(encoding="utf-8")
renderer = (ROOT / "static/js/chatRenderer.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_calendar" in renderer
assert "manage_memory" in renderer assert "manage_memory" in renderer
+2 -1
View File
@@ -5,6 +5,7 @@ import pytest
from src.deep_research import CATEGORY_PROMPTS, _infer_research_category from src.deep_research import CATEGORY_PROMPTS, _infer_research_category
from src.tool_schemas import FUNCTION_TOOL_SCHEMAS from src.tool_schemas import FUNCTION_TOOL_SCHEMAS
from src.visual_report import _standard_visual_variant from src.visual_report import _standard_visual_variant
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1] 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(): 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") 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") routes_source = (ROOT / "routes/research/research_routes.py").read_text(encoding="utf-8")
category_schema = _tool_schema("trigger_research")["parameters"]["properties"]["category"] category_schema = _tool_schema("trigger_research")["parameters"]["properties"]["category"]
rounds_schema = _tool_schema("trigger_research")["parameters"]["properties"]["max_rounds"] rounds_schema = _tool_schema("trigger_research")["parameters"]["properties"]["max_rounds"]