Merge pull request #13 from o3LL/v1/css-split-3

refactor(css): move cookbook, research, memory and settings styles to their own file
This commit is contained in:
Alexandre Teixeira
2026-09-29 14:53:44 +01:00
committed by GitHub
27 changed files with 4195 additions and 3975 deletions
+17
View File
@@ -153,6 +153,23 @@ The helpers below live under `tests/helpers/`. They exist to remove repeated
boilerplate that already appeared across multiple tests. Reach for one only when
your test matches its intended use; do not stretch a helper to cover a new case.
### `tests.helpers.stylesheets.app_css`
Use when a test asserts on a CSS rule.
- Returns every app stylesheet concatenated in the order `static/index.html`
loads them, which is the order the cascade actually has.
- Panel styles no longer all live in `static/style.css`; reading that file
alone ties the test to whichever file a rule sits in today, so it goes red
when a rule moves without the rendered page changing.
- `stylesheet_paths()` and `stylesheet_urls()` are there when a test needs the
files or the request URLs rather than their contents.
`stylesheet_link_tags()` returns the `<link>` markup for a synthetic page
driven through Playwright, so it gets the whole cascade instead of only
`style.css`.
- All of them fail loudly if `index.html` links a stylesheet that is missing.
- Not for vendored CSS under `static/lib/`, which they deliberately skip.
### `tests.helpers.cli_loader.load_script`
Use when a test needs to import a script under `scripts/` without repeating
+63
View File
@@ -0,0 +1,63 @@
"""Read the app's CSS the way the browser does.
``static/style.css`` no longer holds every rule: panel styles live in separate
files that ``static/index.html`` loads eagerly, in a fixed order, right after
it. The cascade is the concatenation of those files in that order.
A test that asserts on a rule must therefore look at all of them. Reading
``static/style.css`` alone ties the test to whichever file a rule happens to
sit in today, so it goes red the next time a rule moves without anything about
the rendered page having changed.
"""
from __future__ import annotations
import re
from pathlib import Path
_STATIC = Path(__file__).resolve().parents[2] / "static"
_INDEX = _STATIC / "index.html"
# Only same-origin app stylesheets. Vendored <link>s under static/lib are not
# part of the cascade these tests reason about.
_LINK = re.compile(
r"""<link\b[^>]*\brel\s*=\s*["']stylesheet["'][^>]*\bhref\s*=\s*["']/static/([^"'?]+)([^"']*)["']""",
re.I,
)
def _entries() -> list[tuple[Path, str]]:
html = _INDEX.read_text(encoding="utf-8")
out = []
for m in _LINK.finditer(html):
rel, query = m.group(1), m.group(2)
if rel.startswith("lib/"):
continue
out.append((_STATIC / rel, "/static/" + rel + query))
if not out:
raise AssertionError(f"no app stylesheet <link> tags found in {_INDEX}")
missing = [p for p, _u in out if not p.is_file()]
if missing:
raise AssertionError(f"index.html links stylesheets that do not exist: {missing}")
return out
def stylesheet_paths() -> list[Path]:
"""Every app stylesheet on disk, in the order index.html loads it."""
return [p for p, _u in _entries()]
def stylesheet_urls() -> list[str]:
"""The same stylesheets as request URLs, query string included."""
return [u for _p, u in _entries()]
def stylesheet_link_tags() -> str:
"""The <link> tags to drop into a synthetic page so it gets the whole
cascade, not just style.css."""
return "".join(f'<link rel="stylesheet" href="{u}">' for u in stylesheet_urls())
def app_css() -> str:
"""The whole cascade as one string, in load order."""
return "\n".join(p.read_text(encoding="utf-8") for p in stylesheet_paths())
+11 -9
View File
@@ -1,6 +1,8 @@
from pathlib import Path
import subprocess
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1]
@@ -80,7 +82,7 @@ def test_email_filter_menu_has_context_title() -> None:
def test_email_setting_toggles_render_neutral_disabled_state() -> None:
source = (ROOT / "static/js/emailLibrary.js").read_text(encoding="utf-8")
style = (ROOT / "static/style.css").read_text(encoding="utf-8")
style = app_css()
assert 'email-settings-auto-reply-section' in source
assert 'email-settings-display-enabled-state' in source
assert 'stateLabel = section?.querySelector' in source
@@ -100,7 +102,7 @@ def test_email_date_headers_mark_unexpected_timeline_gaps() -> None:
assert "email-date-gap-break" in source
assert "gapDays > 90 && gapDays > timelineGapThreshold" in source
style = (ROOT / "static/style.css").read_text(encoding="utf-8")
style = app_css()
assert ".date-section-header.email-date-gap-break" in style
@@ -122,7 +124,7 @@ def test_email_filters_and_card_favorite_toggle_are_wired() -> None:
assert "const typedFilter = _exactTypedFilterSuggestion(v);" in source
assert "_acceptSuggestion(typedFilter);" in source
style = (ROOT / "static/style.css").read_text(encoding="utf-8")
style = app_css()
favorite_start = style.index(".email-card-favorite {")
favorite_end = style.index("}", favorite_start) + 1
assert "top: -3px;" in style[favorite_start:favorite_end]
@@ -154,7 +156,7 @@ def test_email_auto_reply_syncs_one_calendar_event_per_account() -> None:
def test_email_settings_show_away_account_and_compact_display_controls() -> None:
source = (ROOT / "static/js/emailLibrary.js").read_text(encoding="utf-8")
style = (ROOT / "static/style.css").read_text(encoding="utf-8")
style = app_css()
assert 'email-account-away-label">(AWAY)</span>' in source
assert 'id="email-lib-auto-reply-badge"' in source
assert ">Show Email Tags</span>" in source
@@ -227,7 +229,7 @@ def test_email_clean_always_forces_a_fresh_unsubscribe_scan() -> None:
def test_unsubscribe_duplicate_badge_is_lowered() -> None:
frontend = (ROOT / "static/js/emailLibrary.js").read_text(encoding="utf-8")
stylesheet = (ROOT / "static/style.css").read_text(encoding="utf-8")
stylesheet = app_css()
assert "email-unsub-duplicate-badge" in frontend
start = stylesheet.index(".email-unsub-duplicate-badge {")
assert "top: 2px;" in stylesheet[start:stylesheet.index("}", start) + 1]
@@ -235,7 +237,7 @@ def test_unsubscribe_duplicate_badge_is_lowered() -> None:
def test_unsubscribe_scan_status_sits_before_clean_action() -> None:
frontend = (ROOT / "static/js/emailLibrary.js").read_text(encoding="utf-8")
stylesheet = (ROOT / "static/style.css").read_text(encoding="utf-8")
stylesheet = app_css()
start = frontend.index("function _emailCleanupSettingsHtml")
end = frontend.index("function _emailDisplaySettingsHtml", start)
cleanup = frontend[start:end]
@@ -291,7 +293,7 @@ def test_browser_agent_unsubscribe_cleans_sender_after_positive_confirmation() -
def test_auto_unsubscribe_all_is_visibly_taller_than_toolbar_buttons() -> None:
source = (ROOT / "static/style.css").read_text(encoding="utf-8")
source = app_css()
start = source.index(".email-unsub-auto-safe-btn {")
assert "height: 29px;" in source[start:source.index("}", start) + 1]
@@ -330,7 +332,7 @@ def test_unsubscribe_review_marks_handled_cards_and_offers_scan_further() -> Non
def test_unsubscribe_review_can_ignore_a_candidate_without_deleting_it() -> None:
source = (ROOT / "static" / "js" / "emailLibrary.js").read_text()
styles = (ROOT / "static" / "style.css").read_text()
styles = app_css()
assert "email-unsub-ignore-btn" in source
assert "_rememberUnsubscribeIgnored(c)" in source
assert "Ignore this unsubscribe candidate" in source
@@ -339,7 +341,7 @@ def test_unsubscribe_review_can_ignore_a_candidate_without_deleting_it() -> None
def test_email_settings_sections_use_static_headers() -> None:
source = (ROOT / "static" / "js" / "emailLibrary.js").read_text()
styles = (ROOT / "static" / "style.css").read_text()
styles = app_css()
assert 'class="email-unsub-accent-icon"' in source
assert 'M12 0L14.59 8.41' in source
assert 'Scanning ${_esc(scanFolderLabel)} headers…' in source
+3 -1
View File
@@ -1,6 +1,8 @@
from pathlib import Path
import re
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1]
@@ -25,7 +27,7 @@ def test_successful_calendar_tool_output_is_suppressed_in_live_and_saved_rendere
def test_calendar_chat_event_links_fetch_uid_and_show_title_time():
calendar_src = (ROOT / "static/js/calendar.js").read_text()
style_src = (ROOT / "static/style.css").read_text()
style_src = app_css()
routes_src = (ROOT / "routes/calendar_routes.py").read_text()
app_src = (ROOT / "static/app.js").read_text()
renderer_src = (ROOT / "static/js/chatRenderer.js").read_text()
+35 -19
View File
@@ -1,13 +1,25 @@
from pathlib import Path
import re
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1]
def _occurrences(text, needle):
"""Every index of needle, so an assertion does not depend on which copy of
a selector the cascade happens to put first."""
out, i = [], text.find(needle)
while i != -1:
out.append(i)
i = text.find(needle, i + 1)
return out
def test_calendar_week_view_has_overlap_lanes_and_live_ruler_hooks():
calendar_src = (ROOT / "static/js/calendar.js").read_text()
style_src = (ROOT / "static/style.css").read_text()
style_src = app_css()
assert "function _wkLayoutTimedEvents" in calendar_src
assert "--lane:${lane};--lane-count:${laneCount}" in calendar_src
@@ -20,7 +32,7 @@ def test_calendar_week_view_has_overlap_lanes_and_live_ruler_hooks():
def test_crowded_week_events_expand_left_to_reveal_full_title_on_hover():
calendar_src = (ROOT / "static/js/calendar.js").read_text()
style_src = (ROOT / "static/style.css").read_text()
style_src = app_css()
assert "const crowdedClass = laneCount > 1 ? ' cal-wk-block-crowded' : '';" in calendar_src
assert "--lane-right:${laneRight}%" in calendar_src
@@ -61,7 +73,7 @@ def test_crowded_week_events_expand_left_to_reveal_full_title_on_hover():
def test_mobile_truncated_week_event_expands_before_opening_editor():
calendar_src = (ROOT / "static/js/calendar.js").read_text()
style_src = (ROOT / "static/style.css").read_text()
style_src = app_css()
click_idx = calendar_src.index("body.querySelectorAll('.cal-wk-block, .cal-wk-allday-event')")
edit_idx = calendar_src.index("if (ev) _showEventForm(ev);", click_idx)
@@ -78,7 +90,7 @@ def test_mobile_truncated_week_event_expands_before_opening_editor():
def test_mobile_empty_week_slot_selects_before_opening_new_event():
calendar_src = (ROOT / "static/js/calendar.js").read_text()
style_src = (ROOT / "static/style.css").read_text()
style_src = app_css()
assert "let _selectedWeekSlot = null;" in calendar_src
assert "function _paintSelectedWeekSlot(body)" in calendar_src
@@ -100,7 +112,7 @@ def test_mobile_empty_week_slot_selects_before_opening_new_event():
def test_week_view_hints_when_current_time_is_below_the_visible_field():
calendar_src = (ROOT / "static/js/calendar.js").read_text()
style_src = (ROOT / "static/style.css").read_text()
style_src = app_css()
assert "function _updateWeekNowBelowHint(wrap)" in calendar_src
assert "nowRect.top > wrapRect.bottom - 2" in calendar_src
@@ -115,7 +127,7 @@ def test_week_view_hints_when_current_time_is_below_the_visible_field():
def test_mobile_week_scroll_has_resisted_edge_pull_feedback():
calendar_src = (ROOT / "static/js/calendar.js").read_text()
style_src = (ROOT / "static/style.css").read_text()
style_src = app_css()
assert "pullStartedAtTop = _wrap.scrollTop <= 1;" in calendar_src
assert "pullStartedAtBottom = _wrap.scrollTop >= maxScroll - 1;" in calendar_src
@@ -143,7 +155,7 @@ def test_calendar_view_change_recovers_a_fully_open_day_drawer():
def test_calendar_month_and_agenda_have_visual_depth_hooks():
calendar_src = (ROOT / "static/js/calendar.js").read_text()
style_src = (ROOT / "static/style.css").read_text()
style_src = app_css()
assert "cal-weekend" in calendar_src
assert "cal-empty-day" in calendar_src
@@ -156,7 +168,7 @@ def test_calendar_month_and_agenda_have_visual_depth_hooks():
def test_calendar_event_cards_show_compact_source_badges():
calendar_src = (ROOT / "static/js/calendar.js").read_text()
style_src = (ROOT / "static/style.css").read_text()
style_src = app_css()
assert "function _eventSourceHtml" in calendar_src
assert "${_eventSourceHtml(ev)}" in calendar_src
@@ -166,7 +178,7 @@ def test_calendar_event_cards_show_compact_source_badges():
def test_calendar_toolbar_previous_next_arrows_are_mobile_only():
calendar_src = (ROOT / "static/js/calendar.js").read_text()
style_src = (ROOT / "static/style.css").read_text()
style_src = app_css()
assert 'class="cal-nav cal-toolbar-arrow" id="cal-prev"' in calendar_src
assert 'class="cal-nav cal-toolbar-arrow" id="cal-next"' in calendar_src
@@ -181,7 +193,7 @@ def test_calendar_toolbar_previous_next_arrows_are_mobile_only():
def test_calendar_side_arrows_center_against_calendar_pane():
calendar_src = (ROOT / "static/js/calendar.js").read_text()
style_src = (ROOT / "static/style.css").read_text()
style_src = app_css()
assert "function _alignSideNavToCalendar()" in calendar_src
assert "paneRect.top - contentRect.top + (paneRect.height / 2)" in calendar_src
@@ -191,7 +203,7 @@ def test_calendar_side_arrows_center_against_calendar_pane():
def test_calendar_splitter_double_click_snaps_to_nearest_pane_then_toggles():
calendar_src = (ROOT / "static/js/calendar.js").read_text()
style_src = (ROOT / "static/style.css").read_text()
style_src = app_css()
assert "detailH >= calendarH ? 'events' : 'calendar'" in calendar_src
assert "splitter.addEventListener('dblclick', () => {" in calendar_src
@@ -302,7 +314,7 @@ def test_calendar_settings_are_close_only_and_color_the_name_field():
assert name_idx < color_idx
assert "nameInput.style.borderColor = colorInput.value" in calendar_src
assert "nameInput.style.backgroundColor = colorInput.value" not in calendar_src
style_src = (ROOT / "static/style.css").read_text()
style_src = app_css()
toggle_idx = style_src.index(".cal-week-start-toggle")
button_idx = style_src.index(".cal-week-start-btn", toggle_idx)
assert "padding: 0;" in style_src[toggle_idx:button_idx]
@@ -312,7 +324,7 @@ def test_calendar_settings_are_close_only_and_color_the_name_field():
def test_calendar_settings_actions_sync_error_and_escape_behavior():
calendar_src = (ROOT / "static/js/calendar.js").read_text()
style_src = (ROOT / "static/style.css").read_text()
style_src = app_css()
assert calendar_src.count('class="cal-settings-actions"') >= 4
assert "justify-content: flex-end;" in style_src[style_src.index(".cal-settings-actions"):]
@@ -331,7 +343,7 @@ def test_calendar_settings_actions_sync_error_and_escape_behavior():
def test_calendar_hides_navigation_in_agenda_and_event_forms():
calendar_src = (ROOT / "static/js/calendar.js").read_text()
style_src = (ROOT / "static/style.css").read_text()
style_src = app_css()
assert "_modal?.classList.toggle('cal-navigation-hidden', _view === 'agenda')" in calendar_src
assert "_modal?.classList.add('cal-navigation-hidden')" in calendar_src
@@ -353,7 +365,7 @@ def test_calendar_event_form_escape_cancels_before_modal_close():
def test_calendar_from_and_to_month_day_use_theme_highlight():
style_src = (ROOT / "static/style.css").read_text()
style_src = app_css()
month_idx = style_src.index('input[type="date"]::-webkit-datetime-edit-month-field')
day_idx = style_src.index('input[type="date"]::-webkit-datetime-edit-day-field', month_idx)
@@ -364,7 +376,7 @@ def test_calendar_from_and_to_month_day_use_theme_highlight():
def test_calendar_location_hint_uses_accent_map_pin():
calendar_src = (ROOT / "static/js/calendar.js").read_text()
style_src = (ROOT / "static/style.css").read_text()
style_src = app_css()
assert 'class="cal-loc-hint"' in calendar_src
assert '<span>Location</span>' in calendar_src
@@ -382,7 +394,7 @@ def test_calendar_tag_order_prioritizes_personal_work_travel_admin():
def test_long_week_events_keep_their_label_visible_while_scrolling():
calendar_src = (ROOT / "static/js/calendar.js").read_text()
style_src = (ROOT / "static/style.css").read_text()
style_src = app_css()
assert 'class="cal-wk-block-label"' in calendar_src
label_idx = style_src.index(".cal-wk-block-label")
@@ -393,8 +405,12 @@ def test_long_week_events_keep_their_label_visible_while_scrolling():
block_idx = style_src.index(".cal-wk-block {")
assert "overflow: clip;" in style_src[block_idx:label_idx]
head_idx = style_src.index(".cal-wk-col-head {")
allday_idx = style_src.rindex(
".cal-wk-allday {", 0, style_src.index(".cal-wk-allday::-webkit-scrollbar")
# .cal-wk-allday and its scrollbar rule may now sit in different
# stylesheets, so anchoring on the scrollbar rule's offset no longer finds
# the block. Take whichever .cal-wk-allday block carries the z-index.
allday_idx = next(
i for i in _occurrences(style_src, ".cal-wk-allday {")
if "z-index: 39;" in style_src[i:i + 500]
)
now_idx = style_src.index(".cal-wk-now {")
assert "z-index: 40;" in style_src[head_idx:head_idx + 500]
@@ -1,5 +1,7 @@
from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1]
@@ -302,7 +304,7 @@ def test_dependency_status_cache_is_not_fragmented_by_unused_model_hint():
def test_model_lists_fade_after_complete_rows_are_painted():
hwfit = _read("static/js/cookbook-hwfit.js")
cookbook = _read("static/js/cookbook.js")
style = _read("static/style.css")
style = app_css()
assert "cookbook-model-list-fade" in hwfit
assert "cookbook-model-list-fade" in cookbook
@@ -314,7 +316,7 @@ def test_model_lists_fade_after_complete_rows_are_painted():
def test_dependency_panel_has_stable_categories_and_mobile_build_action():
cookbook = _read("static/js/cookbook.js")
style = _read("static/style.css")
style = app_css()
assert "const _depCategoryOrder = ['System', 'Tools', 'LLM', 'Image'" in cookbook
assert "const _orderedDepCategories = (byCat)" in cookbook
@@ -337,7 +339,7 @@ def test_direct_download_auto_fold_requires_deliberate_boundary_gesture():
def test_launch_command_box_uses_accent_focus_glow():
style = _read("static/style.css")
style = app_css()
assert ".hwfit-serve-cmd-details[open] .hwfit-serve-cmd" in style
assert "box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent" in style
@@ -345,7 +347,7 @@ def test_launch_command_box_uses_accent_focus_glow():
def test_server_settings_color_picker_uses_dot_trigger():
cookbook = _read("static/js/cookbook.js")
hwfit = _read("static/js/cookbook-hwfit.js")
style = _read("static/style.css")
style = app_css()
assert 'aria-label="Change server color (currently ${esc(selectedColorLabel)})"' in cookbook
assert "btn.setAttribute('aria-label', `Change server color (currently ${label})`)" in hwfit
assert ".cookbook-server-row .cookbook-srv-color-dot" in style
@@ -357,7 +359,7 @@ def test_server_settings_color_picker_uses_dot_trigger():
def test_default_server_matches_exclusive_model_directory_selector():
cookbook = _read("static/js/cookbook.js")
hwfit = _read("static/js/cookbook-hwfit.js")
style = _read("static/style.css")
style = app_css()
assert "const icon = active ? _MODELDIR_CHECK_ON : _MODELDIR_CHECK_OFF;" in cookbook
assert "_envState.defaultServer = key;" in hwfit
assert "Toggle off if it's already the default" not in hwfit
@@ -2,6 +2,8 @@
from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parent.parent
COOKBOOK = (ROOT / "static/js/cookbook.js").read_text(encoding="utf-8")
@@ -18,7 +20,7 @@ def test_trending_models_expose_persistent_official_only_switch():
def test_trending_models_list_stays_within_the_cookbook_window():
style = (ROOT / "static/style.css").read_text(encoding="utf-8")
style = app_css()
rule = style[style.index("#cookbook-hf-latest-list {"):style.index("#cookbook-hf-latest-list {") + 220]
assert "max-height: min(52vh, 480px);" in rule
@@ -34,7 +36,7 @@ def test_trending_endpoint_applies_first_party_namespace_filter():
def test_official_only_toggle_fits_narrow_download_toolbar():
style = (ROOT / "static/style.css").read_text(encoding="utf-8")
style = app_css()
start = style.rindex(".cookbook-official-filter {")
rule = style[start:style.index("}", start)]
assert "flex: 0 1 auto" in rule
@@ -1,8 +1,10 @@
from pathlib import Path
import re
from tests.helpers.stylesheets import app_css
CSS = Path("static/style.css").read_text()
CSS = app_css()
def rule_for(selector: str) -> str:
@@ -5,10 +5,12 @@ import re
import subprocess
from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1]
DOC_JS = (ROOT / "static/js/document.js").read_text(encoding="utf-8")
STYLE = (ROOT / "static/style.css").read_text(encoding="utf-8")
STYLE = app_css()
def test_font_and_color_controls_use_shared_components():
+3 -1
View File
@@ -4,10 +4,12 @@ import json
import subprocess
from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1]
DOC_JS = (ROOT / "static/js/document.js").read_text(encoding="utf-8")
STYLE = (ROOT / "static/style.css").read_text(encoding="utf-8")
STYLE = app_css()
def test_image_caption_uses_semantic_figure_and_structured_export_paths():
+3 -1
View File
@@ -4,10 +4,12 @@ import json
import subprocess
from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1]
DOC_JS = (ROOT / "static/js/document.js").read_text(encoding="utf-8")
STYLE = (ROOT / "static/style.css").read_text(encoding="utf-8")
STYLE = app_css()
def test_input_rules_are_scoped_to_plain_blocks():
+3 -1
View File
@@ -2,10 +2,12 @@
from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1]
DOC_JS = (ROOT / "static/js/document.js").read_text(encoding="utf-8")
STYLE = (ROOT / "static/style.css").read_text(encoding="utf-8")
STYLE = app_css()
def test_rich_text_toolbar_exposes_document_formatting_controls():
+3 -1
View File
@@ -4,10 +4,12 @@ import json
import subprocess
from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1]
DOC_JS = (ROOT / "static/js/document.js").read_text(encoding="utf-8")
STYLE = (ROOT / "static/style.css").read_text(encoding="utf-8")
STYLE = app_css()
def _run_stats(expression: str):
+4 -2
View File
@@ -1,11 +1,13 @@
from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1]
def test_gallery_detail_uses_fixed_media_stage_and_arrow_lanes():
css = (ROOT / "static/style.css").read_text(encoding="utf-8")
css = app_css()
image_rule = css[css.index(".gallery-detail-image {"):]
image_rule = image_rule[:image_rule.index("}")]
@@ -17,7 +19,7 @@ def test_gallery_detail_uses_fixed_media_stage_and_arrow_lanes():
def test_gallery_media_maximizes_inside_stage_without_resizing_it():
css = (ROOT / "static/style.css").read_text(encoding="utf-8")
css = app_css()
assert css.count("max-height: 100%") >= 2
assert "object-fit: contain" in css
@@ -1,11 +1,13 @@
from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1]
def test_gallery_selection_dot_has_rendered_dimensions():
css = (ROOT / "static/style.css").read_text(encoding="utf-8")
css = app_css()
rule = css[css.index(".gallery-select-dot {"):]
rule = rule[:rule.index("}")]
@@ -15,7 +17,7 @@ def test_gallery_selection_dot_has_rendered_dimensions():
def test_gallery_selection_highlight_is_painted_above_thumbnail():
css = (ROOT / "static/style.css").read_text(encoding="utf-8")
css = app_css()
selector = ".gallery-card:has(.gallery-select-dot.selected)::after"
rule = css[css.index(selector):]
rule = rule[:rule.index("}")]
+10 -8
View File
@@ -1,6 +1,8 @@
from pathlib import Path
import re
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parent.parent
@@ -20,7 +22,7 @@ def test_gallery_exposes_active_filter_organization():
def test_gallery_cards_keep_badges_without_hover_inspect_label():
js = _read("static/js/gallery.js")
css = _read("static/style.css")
css = app_css()
assert "gallery-card-overlay" in js
assert "gallery-card-inspect" not in js
@@ -31,7 +33,7 @@ def test_gallery_cards_keep_badges_without_hover_inspect_label():
def test_gallery_detail_has_richer_inspector_actions():
js = _read("static/js/gallery.js")
css = _read("static/style.css")
css = app_css()
assert "gallery-detail-inspector-head" in js
assert "gallery-detail-meta-grid" in js
@@ -57,7 +59,7 @@ def test_gallery_static_cache_key_bumped():
def test_gallery_bulk_select_button_keeps_toolbar_alignment():
js = _read("static/js/gallery.js")
css = _read("static/style.css")
css = app_css()
assert 'id="gallery-select-btn"' in js
assert 'id="gallery-select-btn" title="Select for bulk actions" style="position:relative;top:0;"' in js
@@ -74,7 +76,7 @@ def test_gallery_bulk_select_button_keeps_toolbar_alignment():
def test_gallery_selection_bars_share_style_and_escape_priority():
gallery = _read("static/js/gallery.js")
ui = _read("static/js/ui.js")
css = _read("static/style.css")
css = app_css()
assert gallery.count("memory-bulk-bar gallery-selection-bar hidden") == 3
assert "window.__galleryCancelSelection = () =>" in gallery
@@ -93,7 +95,7 @@ def test_gallery_selection_bars_share_style_and_escape_priority():
def test_editor_saved_projects_support_grid_and_list_views():
gallery = _read("static/js/gallery.js")
css = _read("static/style.css")
css = app_css()
assert 'id="gallery-editor-drafts-view" role="group"' in gallery
assert 'data-view="grid"' in gallery
@@ -136,7 +138,7 @@ def test_editor_saved_projects_support_grid_and_list_views():
def test_empty_albums_show_action_tiles_instead_of_no_albums_message():
gallery = _read("static/js/gallery.js")
css = _read("static/style.css")
css = app_css()
assert "function _albumActionTiles()" in gallery
assert 'id="gallery-albums-new"' in gallery
@@ -149,7 +151,7 @@ def test_empty_albums_show_action_tiles_instead_of_no_albums_message():
def test_gallery_body_uses_available_window_height_without_grid_crop():
css = _read("static/style.css")
css = app_css()
gallery_section = css.index("/* ── Gallery (image library) ── */")
modal_start = css.index(".gallery-modal-content {", gallery_section)
@@ -161,7 +163,7 @@ def test_gallery_body_uses_available_window_height_without_grid_crop():
def test_gallery_photo_search_is_not_vertically_offset():
css = _read("static/style.css")
css = app_css()
search_idx = css.index(".gallery-search-wrap {")
assert "top: 0;" in css[search_idx:search_idx + 180]
@@ -1,12 +1,14 @@
from pathlib import Path
import re
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parent.parent
def test_mobile_notes_checklist_rows_remain_tappable():
css = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
css = app_css()
notes = (ROOT / "static" / "js" / "notes.js").read_text(encoding="utf-8")
assert "body.notes-mobile-mode .note-card .note-checkbox {" in css
@@ -39,7 +41,7 @@ def test_mobile_long_press_enters_select_mode_for_that_note():
def test_mobile_select_mode_has_subtle_jiggle_feedback():
css = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
css = app_css()
assert "@keyframes notes-select-jiggle" in css
assert "body.notes-mobile-mode .note-card-selectmode" in css
@@ -81,7 +83,7 @@ def test_calendar_email_attachments_have_a_calendar_import_action():
def test_mobile_bulk_select_long_press_is_shared_across_card_types():
helper = (ROOT / "static" / "js" / "mobileBulkSelect.js").read_text(encoding="utf-8")
css = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
css = app_css()
assert ".memory-item[data-memory-id]" in helper
assert ".skill-card[data-skill-name]" in helper
+4 -2
View File
@@ -3,13 +3,15 @@
from pathlib import Path
import re
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parent.parent
def test_history_list_participates_in_shared_library_expansion_layout():
panel = (ROOT / "static/js/research/panel.js").read_text(encoding="utf-8")
css = (ROOT / "static/style.css").read_text(encoding="utf-8")
css = app_css()
assert 'id="research-past-list" class="doclib-grid memory-list research-jobs-list"' in panel
card = css[css.index("#research-pane .research-history-card {"):]
@@ -38,7 +40,7 @@ def test_history_list_participates_in_shared_library_expansion_layout():
def test_expanded_history_report_owns_and_scrolls_the_available_height():
css = (ROOT / "static/style.css").read_text(encoding="utf-8")
css = app_css()
expanded = (
"#research-pane #research-past-list "
+3 -1
View File
@@ -1,5 +1,7 @@
from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1]
@@ -19,7 +21,7 @@ def test_active_research_visualizer_has_live_hierarchy_and_balanced_layout():
def test_active_research_visualizer_respects_reduced_motion():
css = (ROOT / "static/style.css").read_text(encoding="utf-8")
css = app_css()
assert "@media (prefers-reduced-motion: reduce)" in css
assert ".research-synapse .rs-live-dot" in css
+28 -11
View File
@@ -1,11 +1,23 @@
from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parents[1]
def _occurrences(text, needle):
"""Every index of needle, so an assertion does not depend on which copy of
a selector the cascade happens to put first."""
out, i = [], text.find(needle)
while i != -1:
out.append(i)
i = text.find(needle, i + 1)
return out
def test_bulk_select_triggers_share_dot_and_x_icons():
css = (ROOT / "static" / "style.css").read_text()
css = app_css()
for selector in (
"#memory-select-btn",
@@ -36,14 +48,14 @@ def test_library_titles_have_highlighted_respective_icons():
source = (ROOT / "static" / "js" / "documentLibrary.js").read_text()
assert source.count('class="doclib-section-title-icon"') == 4
assert "color: var(--accent, var(--red));" in (ROOT / "static" / "style.css").read_text()
assert "color: var(--accent, var(--red));" in app_css()
for title in ("Chats", "Archive", "Research", "Documents"):
assert f">{title} <span" in source
def test_library_select_follows_tidy():
source = (ROOT / "static" / "js" / "documentLibrary.js").read_text()
css = (ROOT / "static" / "style.css").read_text()
css = app_css()
assert source.index('id="doclib-chats-tidy-btn"') < source.index('id="doclib-chats-select-btn"')
assert source.index('id="doclib-research-tidy-btn"') < source.index('id="doclib-research-select-btn"')
@@ -67,7 +79,7 @@ def test_library_select_follows_tidy():
def test_library_filter_chips_match_skills_count_markup():
source = (ROOT / "static" / "js" / "documentLibrary.js").read_text()
css = (ROOT / "static" / "style.css").read_text()
css = app_css()
assert "function _setLibraryCountChipContent" in source
assert "chip.replaceChildren(text, value)" in source
@@ -89,7 +101,7 @@ def test_launch_cookbook_filter_chips_match_skills():
def test_launch_search_select_and_tags_move_up_together():
css = (ROOT / "static" / "style.css").read_text()
css = app_css()
rule = css[css.index("#serve-search,"):css.index("#serve-search,") + 140]
assert "#hwfit-cache-select," in rule
@@ -109,11 +121,16 @@ def test_launch_search_select_and_tags_move_up_together():
def test_launch_server_selector_and_model_search_move_up_together():
css = (ROOT / "static" / "style.css").read_text()
css = app_css()
rule = css[css.index("#hwfit-server-select,"):css.index("#hwfit-server-select,") + 130]
assert "#hwfit-search" in rule
assert "top: -2px !important;" in rule
# The stylesheets are split across several files, so this selector group
# is not guaranteed to be the first occurrence of its opening selector.
# Check every occurrence instead of a fixed offset into the cascade.
rules = [css[i:i + 130] for i in _occurrences(css, "#hwfit-server-select,")]
assert any(
"#hwfit-search" in rule and "top: -2px !important;" in rule
for rule in rules
), "no #hwfit-server-select rule groups #hwfit-search with top: -2px !important"
def test_launch_heading_uses_accented_flame_icon():
@@ -133,7 +150,7 @@ def test_launch_heading_reports_cached_model_count():
def test_launch_search_matches_document_library_search_treatment():
css = (ROOT / "static" / "style.css").read_text()
css = app_css()
rule = css[css.index("#hwfit-search {", css.index("#hwfit-server-select,")):][:700]
assert "height: 30px;" in rule
@@ -151,7 +168,7 @@ def test_launch_search_matches_document_library_search_treatment():
def test_memory_filter_chips_match_skills_and_are_not_cropped():
source = (ROOT / "static" / "js" / "memory.js").read_text()
css = (ROOT / "static" / "style.css").read_text()
css = app_css()
assert "skills-summary-chip memory-filter-chip" in source
assert "btn.replaceChildren(label, count)" in source
+3 -1
View File
@@ -2,11 +2,13 @@ import re
import subprocess
from pathlib import Path
from tests.helpers.stylesheets import app_css
ROOT = Path(__file__).resolve().parent.parent
SKILLS_JS = (ROOT / "static/js/skills.js").read_text(encoding="utf-8")
METRICS_JS = (ROOT / "static/js/skillsMetrics.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")