Skip to content

Conversation

@Sallie00
Copy link
Contributor

@Sallie00 Sallie00 commented Oct 28, 2025

Summary

This pull request replaces the previous Cypress + Pytest testing setup with a new Playwright (Python) based end-to-end (E2E) testing framework.
The goal of this migration is to simplify the testing environment, improve maintainability, and align all E2E testing with our Python-based architecture.

Changes

Framework Migration

  • Removed Cypress test configuration and dependencies.
  • Removed legacy pytest-based integration tests.
  • Introduced Playwright (Python) as the new E2E testing tool.
  • Updated GitHub Actions workflow to run Playwright tests automatically on CI.

Test Suite Restructure

  • Added new modular Playwright test files under the /tests directory:
tests/
├── conftest.py
├── test.epw
├── test_explorer.py
├── test_filter.py
├── test_natural_ventilation.py
├── test_outdoor.py
├── test_psy_chart.py
├── test_select.py
├── test_summary.py
├── test_sun.py
├── test_t_rh.py
├── test_wind.py
├── utils.py
  • Each test file corresponds to a specific page or feature for better modularity and atomic testing.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added centralized global month/hour filter in Tools menu for streamlined filtering across all analysis pages.
  • Bug Fixes

    • Fixed sun chart month indexing and improved data filtering logic.
  • Testing

    • Migrated test suite from Cypress to Playwright for improved reliability and coverage across all pages.
    • Removed per-page filter UI controls in favor of global filter.
  • Chores

    • Updated UI element naming conventions for consistency (Explorer, Sun, Wind pages).
    • Removed unused CSS utilities and font imports.
    • Updated documentation and contributor credits.

@coderabbitai
Copy link

coderabbitai bot commented Oct 28, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This pull request migrates from Cypress to Playwright testing, introduces a centralized global filter system for month/hour filtering across all pages, refactors UI element IDs from tab-centric naming to scope-based identifiers, removes unused CSS resources, and updates callback signatures across multiple pages to support the new global filter store.

Changes

Cohort / File(s) Summary
CI/CD Workflow Updates
.github/workflows/cypress.yml, .github/workflows/python.yml
Deleted Cypress workflow; added Playwright browser installation and Clima startup steps to Python workflow, replacing Cypress invocation with pytest using base URL argument.
Dependencies
Pipfile
Added pytest-playwright = "*" dependency for Playwright testing support.
CSS Cleanup
assets/banner.css, assets/fonts.css, assets/layout.css
Removed global banner styles (padding, background, color, z-index); deleted three Google Fonts imports (Poppins, Noto Sans JP, Open Sans); removed layout utility classes (.container-col, .container-row, .justify-center, .align-center, .container-stretch, .full-width) and .doc-modal-body img block.
Documentation
docs/README.md, docs/contributing/contributing.md
Added six new contributors; expanded contributing guide with code style sections, updated testing instructions to use Playwright, introduced PR review workflow guidance.
Global Filter Infrastructure
pages/lib/global_element_ids.py, pages/lib/layout.py
Refactored element IDs: TAB6_* → EXPLORER_, TAB4_ → SUN_, TAB5_ → WIND_, TAB7_DROPDOWN → OUTDOOR_DROPDOWN; removed TAB5_CUSTOM_ and TAB2_SCE1_CONTAINER renamed to SUMMARY_SCE1_CONTAINER; added TOOLS_* constants for global filter controls. Implemented global filter state management via create_tools_filter_components(), update_global_filter_state(), get_global_filter_state(), apply_global_month_hour_filter(), and sync_sliders_with_global_state().
Charting & Filter Utilities
pages/lib/charts_data_explorer.py, pages/lib/charts_sun.py, pages/lib/template_graphs.py, pages/lib/utils.py
Added conditional multi-trace heatmap rendering for filtered data; updated month iteration in sun charts from 0-based to 1-based indexing; enhanced wind_rose with skip_time_filter parameter; simplified invert condition logic in determine_month_and_hour_filter.
Page-Level Global Filter Integration
pages/explorer.py, pages/natural_ventilation.py, pages/outdoor.py, pages/psy-chart.py, pages/select.py, pages/summary.py, pages/sun.py, pages/t_rh.py, pages/wind.py
Updated callback signatures across all pages to accept global_filter_data parameter; removed local month/hour filter UI controls; replaced per-section filtering with centralized global filter logic; added TOOLS_GLOBAL_FILTER_STORE input to all affected callbacks; updated element ID references (TAB_* → scope-specific names).
Test Infrastructure Removal
tests/node/cypress.yml, tests/node/cypress/... (e2e, fixtures, support files), tests/node/package.json, tests/python/test_utils.py
Removed entire Cypress test suite, configuration files, fixtures, and related npm manifest; deleted network-dependent Python test module.
Pytest Fixtures
tests/conftest.py
Added session-scoped base_url fixture for Playwright test base URL configuration.
Playwright Test Suite
tests/test_explorer.py, tests/test_filter.py, tests/test_natural_ventilation.py, tests/test_outdoor.py, tests/test_psy-chart.py, tests/test_select.py, tests/test_summary.py, tests/test_sun.py, tests/test_t_rh.py, tests/test_wind.py, tests/utils.py
Added comprehensive Playwright-based test coverage for all major pages (Explorer, Natural Ventilation, Outdoor, Psy Chart, Select, Summary, Sun, T/RH, Wind); introduced UI helpers (upload_epw_file, open_tab, slider manipulation utilities); added parametrized filter interaction tests verifying chart reactivity to month/hour changes.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as Global Filter UI
    participant Store as TOOLS_GLOBAL_FILTER_STORE
    participant Page as Page Callback
    participant Filter as apply_global_month_hour_filter()
    participant Chart as Chart Component

    User->>UI: Adjust month/hour sliders
    User->>UI: Click "Apply month and hour filter"
    UI->>UI: update_global_filter_state()
    UI->>Store: Update store data<br/>(month_range, hour_range,<br/>invert_month, invert_hour,<br/>filter_active)
    Store->>Page: Trigger on data change
    Page->>Filter: apply_global_month_hour_filter(df,<br/>filter_store_data)
    Filter->>Filter: Apply month/hour masks<br/>to DataFrame
    Filter->>Page: Return filtered df
    Page->>Chart: Render with filtered data
    Chart->>User: Display updated chart
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • pages/explorer.py: Multiple callback signature changes (update_tab_yearly, update_tab_daily, update_tab_heatmap, update_more_charts, update_table) with extensive refactoring of filter logic; numerous public ID renames requiring careful verification
  • pages/lib/layout.py: New global filter state management system with five new public functions; critical for data flow correctness across all pages
  • pages/lib/global_element_ids.py: Comprehensive element ID refactoring; must verify all references throughout codebase match the new naming scheme
  • pages/outdoor.py, pages/sun.py, pages/t_rh.py: Complex filter integration requiring verification that global filter state is correctly propagated and applied in each callback
  • tests/test_filter.py: New parametrized test module with slider manipulation logic and multi-page coverage; complex interaction patterns merit thorough review
  • Callback chaining: Verify TOOLS_GLOBAL_FILTER_STORE is properly connected to all consumer callbacks and that filter_active flag is correctly checked in all paths

Possibly related PRs

  • #159 Code sanitisation (Updated) #240: Introduced foundational element ID and column-name refactoring (ElementIds/ColNames enums); this PR builds upon and completes that naming migration across callback signatures and global filter integration.

Poem

🐰 With Playwright's grace, we test the web,
Global filters dance, no local dread,
From TAB6 chaos to EXPLORER's light,
IDs renamed, refactored right,
The grand tapestry rewoven bright!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.08% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "Migrate end-to-end testing framework from Cypress to Playwright" directly aligns with the primary objective stated in the PR summary, which explicitly describes replacing Cypress with a Playwright-based end-to-end testing framework. The title is concise, specific, and clearly communicates the main change: removal of Cypress dependencies and configurations alongside the introduction of Playwright-based tests. While the changeset includes related modifications such as global filter integration and CSS cleanup, the testing framework migration represents the core purpose of the PR, and the title accurately reflects this primary intent without being vague or overly broad.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Sallie00
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Oct 28, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
pages/lib/charts_data_explorer.py (1)

21-28: Avoid pandas chained assignment (potential silent bug).

Use .loc to set filtered values; current df[var][mask] = None risks SettingWithCopy warnings.

Apply:

-            mask = (df[filter_var] < min_val) | (df[filter_var] > max_val)
-            df[var][mask] = None
+            mask = (df[filter_var] < min_val) | (df[filter_var] > max_val)
+            df.loc[mask, var] = None
@@
-            mask = (df[filter_var] >= max_val) & (df[filter_var] <= min_val)
-            df[var][mask] = None
+            mask = (df[filter_var] >= max_val) & (df[filter_var] <= min_val)
+            df.loc[mask, var] = None
pages/summary.py (1)

313-345: Fix x/y length mismatch for HDD/CDD when global month filter is active.

x is derived from df unique months (can be fewer than 12), while y arrays are always length 12 → ValueError in Plotly.

Use a fixed 12‑month x-axis:

-    months = df[Variables.MONTH_NAMES.col_name].unique()
+    from pages.lib.global_scheme import month_lst
+    months = month_lst  # Jan..Dec in order

Alternatively map 1..12 to names: [month_lst[i-1] for i in range(1,13)].

pages/select.py (1)

120-248: Extract repeated filter state dictionary to a constant.

The default global filter state dictionary is duplicated 6 times throughout the submitted_data callback:

{
    "month_range": [1, 12],
    "hour_range": [0, 24],
    "invert_month": [],
    "invert_hour": [],
    "filter_active": False,
}

This violates DRY principle and makes maintenance harder. If the default state structure needs to change, all 6 locations must be updated.

Define a constant at the module level:

DEFAULT_GLOBAL_FILTER_STATE = {
    "month_range": [1, 12],
    "hour_range": [0, 24],
    "invert_month": [],
    "invert_hour": [],
    "filter_active": False,
}

Then replace all occurrences in the return statements with:

return (
    None,
    None,
    True,
    messages_alert["not_available"],
    "orange",
    DEFAULT_GLOBAL_FILTER_STATE.copy(),  # Use .copy() to avoid mutation
)

Note: Use .copy() to ensure each return gets an independent copy of the dictionary.

pages/lib/template_graphs.py (1)

656-663: Critical: Hour filtering uses wrong column name.

Line 662 applies hour filtering to the MONTH column instead of the HOUR column:

time_filtering(
    df_copy, start_hour, end_hour, Variables.MONTH.col_name, target_col  # ← Wrong!
)

This will cause incorrect filtering - hour-based filters will mask data based on month values instead of hour values.

Apply this fix:

 time_filtering(
     df_copy, start_month, end_month, Variables.MONTH.col_name, target_col
 )
 time_filtering(
-    df_copy, start_hour, end_hour, Variables.MONTH.col_name, target_col
+    df_copy, start_hour, end_hour, Variables.HOUR.col_name, target_col
 )
pages/outdoor.py (1)

159-195: Harden display-name formatting and add fallback

Current replacements can miss variants and may return an empty list (no zeros). Suggest regex-based normalization and a fallback message.

Apply:

@@
-    display_names = []
-    for col in cols_with_the_highest_number_of_zero:
-        # Remove utci_ prefix and replace all underscores with spaces
-        display_name = col.replace("utci_", "UTCI ")
-        display_name = display_name.replace("_", " ")
-        display_name = display_name.replace("categories", "Categories")
-        # Fix specific words that need spaces
-        display_name = display_name.replace("noSun", "No Sun")
-        display_name = display_name.replace("noWind", "No Wind")
-        display_name = display_name.replace("Sun", "Sun")  # Keep Sun as is
-        display_name = display_name.replace("Wind", "Wind")  # Keep Wind as is
-        display_names.append(display_name)
-
-    return f"The Best Weather Condition is: {', '.join(display_names)}"
+    import re
+    def _fmt(name: str) -> str:
+        s = re.sub(r'^utci_', 'UTCI ', name, flags=re.I)
+        s = s.replace('_', ' ')
+        s = re.sub(r'\bcategories\b', 'Categories', s, flags=re.I)
+        s = re.sub(r'\bno\s*sun\b', 'No Sun', s, flags=re.I)
+        s = re.sub(r'\bno\s*wind\b', 'No Wind', s, flags=re.I)
+        return s
+    display_names = [_fmt(c) for c in cols_with_the_highest_number_of_zero]
+    if not display_names:
+        return "The Best Weather Condition is: No clear winner"
+    return f"The Best Weather Condition is: {', '.join(display_names)}"
🧹 Nitpick comments (30)
Pipfile (1)

19-19: Scope and pin pytest-playwright as a dev dependency.

Keep test-only deps out of runtime and avoid floating versions.

Apply this diff:

 [packages]
 ...
-scipy = "==1.12.0"
-pytest-playwright = "*"
+scipy = "==1.12.0"

 [dev-packages]
 cleanpy = "*"
 pytest = "*"
+pytest-playwright = ">=0.5,<0.6"
+# (optional) pin Playwright itself to stabilize browser/protocol pairing
+# playwright = ">=1.45,<1.46"
 bump2version = "*"
 black = "*"
 ruff = "*"
 pre-commit = "*"
docs/README.md (1)

37-42: Unify contributor formatting for consistency.

Use the same linked-name style for all new entries and fix capitalization (e.g., “Wenshu Lyu”). Optionally add profile links for each entry for parity with prior items.

tests/conftest.py (1)

4-6: Provide default and clear error for missing --base-url.

Helps local runs and avoids cryptic None usage.

Apply this diff:

 @pytest.fixture(scope="session")
-def base_url(pytestconfig):
-    return pytestconfig.getoption("base_url")
+def base_url(pytestconfig) -> str:
+    url = pytestconfig.getoption("base_url")
+    if not url:
+        # Default aligned with CI/docs; make it easy to override
+        url = "http://127.0.0.1:8080"
+    return url
.github/workflows/python.yml (1)

34-37: Optional: use the official Playwright Action for reliability.

It installs browsers and OS deps without sudo prompts.

Use:

- name: Setup Playwright
  uses: microsoft/playwright-github-action@v1
  with:
    with-deps: true

Then drop the manual “Install Playwright Browsers” step.

docs/contributing/contributing.md (2)

80-111: Clarify formatter: Ruff vs Black.

Section says Ruff enforces formatting, then instructs to install/run Black. Pick one to avoid conflicting edits, or mark Black as optional.

Apply this diff to rely on Ruff only:

-Install Black:
-
-```bash
-pipenv install black
-```
-
-Format your code before committing:
-
-```bash
-black .
-```
+Ruff includes a formatter; no need for Black. If your editor doesn’t support Ruff fmt yet, it’s fine to use Black locally, but CI will enforce Ruff’s formatting.

133-144: Testing commands: add OS deps and a startup wait; align on port.

  • Use --with-deps for Linux devs.
  • Ensure the server port matches the --base-url.

Apply this diff:

 pipenv sync
-pipenv run playwright install  # Required to install browsers
+pipenv run playwright install --with-deps  # Installs browsers and OS deps (Linux)

-Start the app server:
+Start the app server (ensure it listens on 8080 to match the pytest base URL):
 ```bash
-pipenv run python main.py
+pipenv run python main.py --port 8080

-Then, from the root directory, run:
+Then, from the root directory, wait for the server and run tests:

-cd tests
-
-pipenv run pytest --base-url=http://127.0.0.1:8080
+until curl -fsS http://127.0.0.1:8080 >/dev/null; do sleep 1; done
+cd tests && pipenv run pytest --base-url=http://127.0.0.1:8080

</blockquote></details>
<details>
<summary>tests/test_wind.py (1)</summary><blockquote>

`40-42`: **Fix incomplete comment.**

The comment is grammatically incomplete and should be revised for clarity.



Apply this diff:

```diff
-def test_wind_rose_texts(page: Page):
-    """Verify that all Wind Rose description texts"""
+def test_wind_rose_texts(page: Page):
+    """Verify that all Wind Rose description texts are correct"""
tests/utils.py (2)

17-20: Hardcoded location string creates brittle test dependency.

The assertion checks for a specific location "Bologna Marconi AP, ITA" which is tightly coupled to the test.epw file. If the test EPW file is replaced or updated, this assertion will fail.

Consider making the location check more flexible or parametrizing the expected location.

Apply this diff to make the check more flexible:

-    # Verify that the upload success messages are displayed
     expect(page.get_by_text("The EPW was successfully loaded!")).to_be_visible()
-    expect(
-        page.get_by_text("Current Location: Bologna Marconi AP, ITA")
-    ).to_be_visible()
+    # Verify that a location was loaded (any location is valid for tests)
+    expect(page.get_by_text("Current Location:")).to_be_visible()

23-37: Potential selector issues with special characters in tab names.

The string interpolation in the selector at Line 29 could fail if tab_name contains special characters like quotes or brackets that have special meaning in CSS selectors.

While this is unlikely in the current test suite, consider using a more robust selector approach or escaping special characters.

If this becomes an issue, you can use Playwright's text locator with exact matching:

def open_tab(page: Page, tab_name: str):
    """
    Open a specific tab from the sidebar navigation (default expanded version).
    Works reliably for Mantine NavLink structure.
    """
    # Find by role and accessible name for more robustness
    nav_link = page.get_by_role("link", name=tab_name).or_(
        page.get_by_role("button", name=tab_name)
    )
    
    nav_link.scroll_into_view_if_needed()
    expect(nav_link).to_be_visible()
    nav_link.click()
pages/lib/charts_data_explorer.py (2)

89-96: Unify hover month labeling (names vs numbers).

Filtered branch uses MONTH; unfiltered uses MONTH_NAMES. Prefer names in both for consistency.

Apply:

-                    customdata=np.stack(
-                        (df[Variables.MONTH.col_name], df[Variables.DAY.col_name]),
-                        axis=-1,
-                    ),
+                    customdata=np.stack(
+                        (df[Variables.MONTH_NAMES.col_name], df[Variables.DAY.col_name]),
+                        axis=-1,
+                    ),

And keep MONTH_NAMES in the unfiltered branch as-is.

Also applies to: 143-146


67-101: Drop redundant inner check.

Outer condition already ensures any filtered rows exist; remove the inner if filtered_mask.any() to simplify.

Apply:

-    if has_filter_marker and df["_is_filtered"].any():
-        filtered_mask = df["_is_filtered"]
-        if filtered_mask.any():
-            original_col = f"_{var}_original"
-            ...
-            fig.add_trace(...)
+    if has_filter_marker and df["_is_filtered"].any():
+        filtered_mask = df["_is_filtered"]
+        original_col = f"_{var}_original"
+        ...
+        fig.add_trace(...)
tests/test_summary.py (1)

49-61: Either click and verify download, or update the docstring.

Current test checks visibility only while claiming “clickable.”

Example to assert download:

with page.expect_download() as dl:
    page.get_by_role("button", name="Download EPW").click()
download = dl.value
assert download.suggested_filename.endswith(".epw")
pages/summary.py (3)

298-305: Pass a list of columns to the global filter for consistency.

Other call sites pass a list; keep API uniform.

Apply:

-        df = apply_global_month_hour_filter(
-            df, global_filter_data, Variables.DBT.col_name
-        )
+        df = apply_global_month_hour_filter(
+            df, global_filter_data, [Variables.DBT.col_name]
+        )

317-327: Avoid hard‑coded column names in queries.

Use Variables.* to prevent drift if names change.

Apply:

-        query_month = "month=="
-        a = df.query(query_month + str(i) + " and DBT<=" + str(hdd_setpoint))[Variables.DBT.col_name].sub(hdd_setpoint)
+        query_month = f"{Variables.MONTH.col_name}=="
+        a = df.query(f"{query_month}{i} and {Variables.DBT.col_name}<={hdd_setpoint}")[Variables.DBT.col_name].sub(hdd_setpoint)
@@
-        a = df.query(query_month + str(i) + " and DBT>=" + str(cdd_setpoint))[Variables.DBT.col_name].sub(cdd_setpoint)
+        a = df.query(f"{query_month}{i} and {Variables.DBT.col_name}>={cdd_setpoint}")[Variables.DBT.col_name].sub(cdd_setpoint)

201-204: Trim unintended leading spaces in longitude label.

Minor UX nit: extra spaces before “Longitude”.

Apply:

-    lon = f"    Longitude: {meta[Variables.LON.col_name]}"
+    lon = f"Longitude: {meta[Variables.LON.col_name]}"
tests/test_outdoor.py (1)

40-47: Actually select the option and wait for charts to reload.

Currently you locate the option but never click nor wait for callback completion.

Example:

dropdown = page.locator("#outdoor-dropdown")
dropdown.click()
page.get_by_text("UTCI: Sun & no Wind", exact=True).click()

# Wait for Dash loading cycle to finish
loading = page.locator('[data-dash-is-loading="true"]')
loading.first.wait_for(state="detached", timeout=10000)

for sel in ("#utci-heatmap", "#utci-category-heatmap", "#utci-summary-chart"):
    expect(page.locator(sel)).to_be_visible()
pages/t_rh.py (1)

103-117: Global filter integration looks good; consider minor DRY/defensive tweaks.

  • Factor target_columns into a shared helper/constant to avoid duplication.
  • If stores can ever serialize df to dict, defensively coerce to DataFrame as done in summary.py.

Also applies to: 152-158, 217-233, 276-286

pages/natural_ventilation.py (1)

225-246: Move imports to module level and extract duplicate logic.

The dynamic imports from pages.lib.layout inside the callback are repeated in both nv_heatmap and nv_bar_chart callbacks. This pattern has several drawbacks:

  • Import overhead on every callback execution
  • Code duplication across callbacks
  • Harder to maintain consistency

Consider these improvements:

  1. Move the imports to the top of the file alongside other imports
  2. Extract the global filter checking and state extraction into a shared helper function

Apply this diff to move imports to module level:

 from pages.lib.utils import (
     title_with_tooltip,
     generate_chart_name,
     generate_units_degree,
     generate_units,
     generate_custom_inputs_nv,
     determine_month_and_hour_filter,
     title_with_link,
 )
+from pages.lib.layout import (
+    apply_global_month_hour_filter,
+    get_global_filter_state,
+)

Then extract a helper function (add after imports):

def get_filter_ranges(global_filter_data):
    """Extract month and hour ranges from global filter data or return defaults."""
    if global_filter_data and global_filter_data.get("filter_active", False):
        filter_state = get_global_filter_state(global_filter_data)
        month_range = filter_state["month_range"]
        hour_range = filter_state["hour_range"]
        invert_month_global = filter_state["invert_month"]
        invert_hour_global = filter_state["invert_hour"]
        
        return determine_month_and_hour_filter(
            month_range, hour_range, invert_month_global, invert_hour_global
        )
    else:
        return 1, 12, 0, 24

Then replace lines 225-246 and similar code in nv_bar_chart with:

start_month, end_month, start_hour, end_hour = get_filter_ranges(global_filter_data)

if global_filter_data and global_filter_data.get("filter_active", False):
    df = apply_global_month_hour_filter(
        df, global_filter_data, Variables.DBT.col_name
    )
tests/test_t_rh.py (1)

56-68: Test assertion may be brittle and non-specific.

The test relies on finding the text "-40" (with exact=False) to verify the Global/Local toggle worked. This approach has several issues:

  • The -40 value might appear in multiple places on the page
  • exact=False makes the assertion even less specific (matches "-40.5", "-400", etc.)
  • The test doesn't explain why "-40" indicates Global mode
  • Changes to the data or display format could break this test

Consider using a more specific selector or assertion that:

  1. Targets a specific element by ID or data-testid
  2. Checks a more distinctive value or UI state
  3. Adds a comment explaining what "-40" represents

Example improvement:

def test_banner_unit_switch(page: Page):
    """
    Verify that the banner radio buttons (Global/Local) correctly toggle.
    """
    nav_controls = page.locator("#nav-group-controls")
    nav_controls.click(force=True)

    global_button = page.get_by_text("Global", exact=True)
    global_button.scroll_into_view_if_needed()
    global_button.wait_for(state="visible")
    global_button.click(force=True)

    # Verify Global mode is active by checking a chart displays global range minimum
    # Global temperature range starts at -40°C
    chart_element = page.locator("#yearly-chart")
    expect(chart_element).to_be_visible()
    # Alternative: Check for a more specific element or attribute that indicates Global mode
pages/psy-chart.py (1)

187-209: Move imports to module level to improve performance.

Similar to other files in this PR, the dynamic imports from pages.lib.layout inside the callback create unnecessary overhead on every execution.

Move the imports to the module level:

 from pages.lib.utils import (
     generate_chart_name,
     generate_units,
     generate_custom_inputs_psy,
     determine_month_and_hour_filter,
     title_with_link,
     dropdown,
 )
+from pages.lib.layout import (
+    apply_global_month_hour_filter,
+    get_global_filter_state,
+)

This is a consistent pattern across the codebase and should be applied uniformly to all callback files.

pages/lib/template_graphs.py (1)

542-664: Extract duplicate heatmap trace creation logic.

The heatmap and heatmap_with_filter functions contain nearly identical multi-trace creation logic (lines 558-622 and 419-483 respectively). This duplication makes maintenance harder and increases the risk of inconsistencies.

Consider extracting the trace creation logic into a shared helper function:

def _create_heatmap_traces(df, var, var_color, var_unit, range_z, y_values):
    """Create heatmap traces with optional filtered data overlay."""
    fig = go.Figure()
    has_filter_marker = "_is_filtered" in df.columns
    
    if has_filter_marker and df["_is_filtered"].any():
        # Create filtered trace
        # ... existing logic ...
        
        # Create normal trace
        # ... existing logic ...
    else:
        # Create single trace
        # ... existing logic ...
    
    return fig

Then both heatmap and heatmap_with_filter can call this helper with appropriate y-values.

pages/lib/layout.py (1)

553-604: Inconsistent data format for invert flags creates confusion.

The invert flags undergo multiple conversions between boolean and list format:

  1. UI switches store as boolean (checked property)
  2. Line 580-581: Converted to list ["invert"] or [] for storage
  3. Line 602-603: Converted back to boolean in get_global_filter_state

This inconsistency makes the code harder to understand and maintain. The list format ["invert"] appears to be treating the field like a checklist value, but a simple boolean would be clearer.

Consider standardizing on boolean throughout:

 def update_global_filter_state(
     apply_clicks, month_range, hour_range, invert_month, invert_hour, current_data
 ):
     if apply_clicks is None:
         apply_clicks = 0

     if apply_clicks > 0:
         current_data["filter_active"] = True

         current_data.update(
             {
                 "month_range": month_range or [1, 12],
                 "hour_range": hour_range or [0, 24],
-                "invert_month": ["invert"] if invert_month else [],
-                "invert_hour": ["invert"] if invert_hour else [],
+                "invert_month": bool(invert_month),
+                "invert_hour": bool(invert_hour),
             }
         )

     return current_data


 def get_global_filter_state(filter_store_data):
     if not filter_store_data:
         return {
             "filter_active": False,
             "month_range": [1, 12],
             "hour_range": [0, 24],
             "invert_month": False,
             "invert_hour": False,
         }

     return {
         "filter_active": filter_store_data.get("filter_active", False),
         "month_range": filter_store_data.get("month_range", [1, 12]),
         "hour_range": filter_store_data.get("hour_range", [0, 24]),
-        "invert_month": bool(filter_store_data.get("invert_month", [])),
-        "invert_hour": bool(filter_store_data.get("invert_hour", [])),
+        "invert_month": filter_store_data.get("invert_month", False),
+        "invert_hour": filter_store_data.get("invert_hour", False),
     }

This would also require updating the default store data in create_stores (lines 433-439).

pages/wind.py (1)

242-251: Move imports to module level for consistency.

The dynamic imports from pages.lib.layout should be moved to module level for consistency with best practices and to avoid import overhead on every callback execution.

Add at the top of the file with other imports:

 from pages.lib.utils import (
     generate_chart_name,
     generate_units,
     title_with_link,
 )
+from pages.lib.layout import apply_global_month_hour_filter

Then remove the dynamic import statements from the callbacks (lines 243-245, 284, 313, 466-468).

pages/sun.py (2)

199-211: Optional: import hoist to reduce per-callback import churn

If there’s no circular dependency, move apply_global_month_hour_filter import to module scope; else keep as-is.

Also applies to: 261-279, 316-320, 346-349


314-320: Apply row-removal safeguard for consistency with monthly_and_cloud_chart

monthly_and_cloud_chart removes rows marked _is_filtered, but daily() and update_heatmap() don't. This inconsistency could cause calculations in daily_profile and heatmap to include filtered data. Apply the safeguard post-filter in both locations:

     if global_filter_data and global_filter_data.get("filter_active", False):
         from pages.lib.layout import apply_global_month_hour_filter
 
         df = apply_global_month_hour_filter(df, global_filter_data, var)
+        if isinstance(df, object) and hasattr(df, "columns") and "_is_filtered" in df.columns:
+            df = df[~df["_is_filtered"]]

Apply to: lines 314–320 (daily) and 345–349 (update_heatmap).

pages/outdoor.py (1)

220-247: DRY up repeated global-filter state extraction

The month/hour/invert extraction repeats across three callbacks. Consider a small helper (e.g., resolve_filter_state(global_filter_data)) returning time_filter, month, hour, invert flags.

Happy to factor this into pages/lib/layout.py if desired.

Also applies to: 307-336, 394-421

tests/test_filter.py (2)

145-171: Stabilize baseline and hashing to reduce flakiness

  • Set the filter to a known baseline before capturing base_hash.
  • Prefer a stable digest over built-in hash().

Apply:

@@ def assert_chart_changes_by_three_steps(page: Page, chart_selector: str):
-    base_hash = _chart_state_hash(page, chart_selector)
+    # Ensure a known baseline before hashing
+    apply_filter(page, BASELINE_MONTH, BASELINE_HOUR)
+    base_hash = _chart_state_hash(page, chart_selector)
@@
-    assert changed, (
+    assert changed, (
         f"Chart did not change after filter steps for selector {chart_selector}"
     )

And:

@@ def _chart_state_hash(page: Page, chart_selector: str) -> str:
-    html = node.inner_html()
-    return str(hash(html))
+    import hashlib
+    html = node.inner_html()
+    return hashlib.blake2s(html.encode("utf-8")).hexdigest()

185-189: Add storage clearing to fixture to prevent state leakage between tests

The codebase uses Mantine components (in pages/lib/layout.py, pages/explorer.py, etc.), which provide useSessionStorage and useLocalStorage hooks that persist data in the browser. With the default pytest-playwright configuration, the browser context is shared across tests, so sessionStorage/localStorage will persist between test runs on the same context. Add storage clearing to the fixture to ensure isolation:

@pytest.fixture(scope="function", autouse=True)
def _bootstrap_each(page: Page, base_url: str):
    page.context.clear_cookies()
    page.evaluate('sessionStorage.clear(); localStorage.clear();')
    page.goto(base_url)
    upload_epw_file(page)
    yield
pages/explorer.py (2)

469-476: Duplicate docstring line—remove the repeated sentence

There are two identical docstrings back-to-back.

-    """Update the contents of tab size. Passing in the info from the dropdown and the general info."""
-    """Update the contents of tab size. Passing in the info from the dropdown and the general info."""
+    """Update the contents of tab size. Passing in the info from the dropdown and the general info."""

528-553: Avoid double time-filtering (optional)

You filter df via apply_global_month_hour_filter and then also pass a time_filter_info that re-applies ranges inside custom_heatmap. Consider setting time_filter_info to [False, [1,12], [0,24]] after pre-filtering to reduce work.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fb9de71 and 6cec562.

⛔ Files ignored due to path filters (2)
  • Pipfile.lock is excluded by !**/*.lock
  • tests/node/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (44)
  • .github/workflows/cypress.yml (0 hunks)
  • .github/workflows/python.yml (1 hunks)
  • Pipfile (1 hunks)
  • assets/banner.css (0 hunks)
  • assets/fonts.css (0 hunks)
  • assets/layout.css (0 hunks)
  • docs/README.md (1 hunks)
  • docs/contributing/contributing.md (4 hunks)
  • pages/explorer.py (17 hunks)
  • pages/lib/charts_data_explorer.py (1 hunks)
  • pages/lib/charts_sun.py (5 hunks)
  • pages/lib/global_element_ids.py (5 hunks)
  • pages/lib/layout.py (8 hunks)
  • pages/lib/template_graphs.py (6 hunks)
  • pages/lib/utils.py (1 hunks)
  • pages/natural_ventilation.py (6 hunks)
  • pages/outdoor.py (6 hunks)
  • pages/psy-chart.py (2 hunks)
  • pages/select.py (6 hunks)
  • pages/summary.py (4 hunks)
  • pages/sun.py (6 hunks)
  • pages/t_rh.py (4 hunks)
  • pages/wind.py (5 hunks)
  • tests/conftest.py (1 hunks)
  • tests/node/cypress.config.js (0 hunks)
  • tests/node/cypress/.gitignore (0 hunks)
  • tests/node/cypress/e2e/spec.cy.js (0 hunks)
  • tests/node/cypress/fixtures/example.json (0 hunks)
  • tests/node/cypress/support/commands.js (0 hunks)
  • tests/node/cypress/support/e2e.js (0 hunks)
  • tests/node/package.json (0 hunks)
  • tests/node/test.epw (0 hunks)
  • tests/python/test_utils.py (0 hunks)
  • tests/test_explorer.py (1 hunks)
  • tests/test_filter.py (1 hunks)
  • tests/test_natural_ventilation.py (1 hunks)
  • tests/test_outdoor.py (1 hunks)
  • tests/test_psy-chart.py (1 hunks)
  • tests/test_select.py (1 hunks)
  • tests/test_summary.py (1 hunks)
  • tests/test_sun.py (1 hunks)
  • tests/test_t_rh.py (1 hunks)
  • tests/test_wind.py (1 hunks)
  • tests/utils.py (1 hunks)
💤 Files with no reviewable changes (13)
  • assets/banner.css
  • tests/node/cypress/fixtures/example.json
  • tests/node/cypress/support/e2e.js
  • tests/node/cypress/support/commands.js
  • assets/layout.css
  • tests/node/cypress.config.js
  • tests/node/test.epw
  • assets/fonts.css
  • .github/workflows/cypress.yml
  • tests/node/cypress/.gitignore
  • tests/node/cypress/e2e/spec.cy.js
  • tests/node/package.json
  • tests/python/test_utils.py
🔇 Additional comments (18)
.github/workflows/python.yml (1)

38-41: Remove the --port flag suggestion; it's not supported by the app. Port is already hardcoded to 8080.

The app does not accept a --port CLI argument—AppConfig.PORT is hardcoded to 8080 and passed directly to app.run(). The suggested diff with --port ${CLIMA_PORT} will not work. However, adding a readiness check is still valid to prevent test flakiness, and using working-directory instead of cd tests is a good improvement. Revise the suggestion to remove CLI flag handling while keeping the HTTP readiness check and working-directory change.

Likely an incorrect or invalid review comment.

tests/test_sun.py (1)

1-62: LGTM! Well-structured Playwright tests.

The test suite follows best practices with:

  • Clear setup fixture for navigation and EPW upload
  • Well-organized test functions covering different aspects (titles, controls, graphs, interactions)
  • Appropriate use of Playwright's expect assertions
  • Good element selector strategies (IDs and text matching)
tests/test_psy-chart.py (1)

1-42: LGTM! Consistent test implementation.

The Psychrometric Chart tests follow the established patterns from other test files with good coverage of UI elements, controls, and interactions.

tests/test_wind.py (1)

1-55: LGTM! Comprehensive wind rose testing.

The test suite effectively validates wind rose visualizations and their associated text content. The use of dictionaries for expected text values and loops for checking multiple elements is clean and maintainable.

tests/test_select.py (1)

1-44: LGTM! Complete EPW upload workflow testing.

The test suite provides good coverage of the EPW file selection and upload functionality, from initial page load through successful upload and map rendering.

pages/lib/charts_sun.py (1)

36-124: LGTM! Excellent refactoring for clarity.

The change from 0-based indexing with i+1 adjustments to explicit 1-based month numbering with range(1, 13) significantly improves readability. The introduction of col_idx as an alias and is_first derived from it makes the code intent clearer. All data filters and subplot placements have been updated consistently.

tests/utils.py (1)

1-37: Useful test utilities with good documentation.

The helper functions are well-documented and provide essential functionality for the test suite. The EPW upload verification and tab navigation helpers will reduce duplication across test files.

tests/test_natural_ventilation.py (1)

1-60: LGTM! Thorough testing of Natural Ventilation interactions.

The test suite provides excellent coverage including:

  • Static element visibility checks
  • Interactive controls validation
  • State-dependent behavior (condensation checkbox enabling/disabling the dew point filter)

The tests are well-organized and follow established patterns.

pages/lib/utils.py (1)

269-277: No action required—this change improves correctness.

The behavioral change from equality check to truthiness is safe and actually fixes a latent bug. The codebase stores filter inversion state as either:

  • ["invert"] (non-empty list) when inversion is enabled
  • [] (empty list) when disabled
  • False (boolean) as fallback default

The old condition invert_month == Variables.INVERT.col_name (string equality) would fail when receiving ["invert"] from update_global_filter_state, preventing inversion from working. The new truthiness check if invert_month and ... correctly handles all three value types and fixes this bug. All callers pass appropriate values from filter_state dictionaries.

pages/summary.py (1)

268-289: Original review concern is not valid; no action needed.

The apply_global_month_hour_filter function is intentionally designed to accept multiple input types. Its implementation includes defensive code that:

  • Converts string arguments to lists: elif isinstance(target_columns, str): target_columns = [target_columns]
  • Provides sensible default: if target_columns is None: target_columns = [Variables.DBT.col_name]

The mixed calling patterns observed across the codebase (some passing lists, some passing single strings, some passing nothing) are all handled correctly by the function. No consistency enforcement is needed.

Likely an incorrect or invalid review comment.

pages/sun.py (3)

126-136: SUN id migration + wiring looks good

IDs switched to SUN_* consistently and outputs point to the new containers. No functional concerns spotted.

Also applies to: 301-307, 331-337


188-214: Global filter application + safe drop of flagged rows

Good use of apply_global_month_hour_filter with a targeted column set, and guarding with "_is_filtered" removal before aggregations.


258-279: Target column set for sun path is comprehensive; keep an eye on optional var

Appending var when not "None" is correct. If var can alias to a derived field, ensure it exists in df before filtering to avoid KeyError.

Run to spot any non-existent var values reaching this callback:

pages/outdoor.py (2)

47-56: OUTDOOR_DROPDOWN migration is clean

Renamed id and defaults/persistence are sensible.


349-369: Index logic is correct; review concern not founded.

The trace ordering in heatmap_with_filter is deterministic:

  • When filter markers exist and filtered data is present: filtered trace (index 0), then normal heatmap (index 1)
  • Otherwise: single normal heatmap (index 0)

The current colorbar_index = 1 if len(utci_stress_cat["data"]) > 1 else 0 safely applies the colorbar to the correct trace in both cases. The suggested refactor using trace.type == "heatmap" is more defensive but unnecessary here since ordering is guaranteed.

Likely an incorrect or invalid review comment.

pages/explorer.py (3)

214-245: Explorer id renames and section scaffolding look consistent

EXPLORER_* ids replace TAB6_* cleanly; containers and inputs align with tests.

Also applies to: 248-345, 616-632


720-737: Table: good use of _is_filtered guard

Filtering then dropping flagged rows prevents empty columns. LGTM.


654-661: Local fallback path is sensible

Using filter_df_by_month_and_hour with full column set is a safe default when the global filter is inactive.

@Sallie00 Sallie00 marked this pull request as ready for review October 28, 2025 08:24
@Sallie00
Copy link
Contributor Author

I have submitted the pull request for the newly added automated tests and workflow updates.The GitHub Actions pipeline has successfully passed all tests and checks. Please kindly review the PR when you have a moment. Thank you for your time and feedback. @FedericoTartarini

@FedericoTartarini
Copy link
Contributor

@Sallie00 could you please pull the changes from development so I can review the pull request

@FedericoTartarini
Copy link
Contributor

@Sallie00 a few tests are failing

@Sallie00
Copy link
Contributor Author

Sallie00 commented Nov 4, 2025

@FedericoTartarini I’ve addressed the previous errors and made the necessary fixes. This PR is now ready for your review.

@FedericoTartarini
Copy link
Contributor

@Sallie00 I will approve it now since all the test are passing, however, I would like to ask you to look into how we can speed up or parallelise the tests. Currently it takes more than 10 minutes to run the tests, it should ideally not take this long to do that. Could you please look into solutions on how to speed them up? I tried installing pytest-xdist but it not working. Could you please help me with this?

@FedericoTartarini FedericoTartarini merged commit bf5a558 into CenterForTheBuiltEnvironment:development Nov 4, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants