Skip to content

Conversation

@saurabh24thakur
Copy link

@saurabh24thakur saurabh24thakur commented Jan 6, 2026

Checklist

  • I have read the OpenWISP Contributing Guidelines.
  • I have manually tested the changes proposed in this pull request.
  • I have written new test cases for new code and/or updated existing tests for changes to existing code.
  • I have updated the documentation.

Reference to Existing Issue

Closes #475

Description of Changes

This PR implements a comprehensive Dark Mode feature for the NetJSONGraph map interface.

Key Changes:

  1. Built-in Theme Toggle: Added a new "Toggle Theme" button directly to the map GUI (top-right corner), removing the need for external implementation.
  2. Map Tile Integration: The Leaflet map now dynamically switches to CartoDB Dark Matter tiles when dark mode is enabled, ensuring the map visual matches the UI theme.
  3. UI Styling: Updated netjsongraph-theme.css to provide dark mode styles for the sidebar, tooltips, metadata container, and the new toggle button.
  4. Persistence: The theme preference is saved in localStorage (key: map_theme), persisting across page reloads.
  5. Testing:
    • Added test/darkmode.test.js to verify theme toggling and persistence.
    • Updated test/netjsongraph.render.test.js and test/netjsongraph.dom.test.js to cover new logic and fix existing test gaps.

@coderabbitai
Copy link

coderabbitai bot commented Jan 6, 2026

📝 Walkthrough

Walkthrough

Adds dark-mode support across UI and map rendering: new dark tile config (mapTileConfigDark), CSS .dark-mode theme rules and a .njg-themeControl toggle, GUI method createThemeControl() (persisting theme to localStorage and toggling dark-mode class), util method isDarkMode(self), and render changes to pick and replace Leaflet tile layers based on dark mode while guarding Leaflet-dependent operations. Tests added/updated to validate theme control presence and tile selection. No public APIs removed.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant User
    participant GUI as NetJSONGraphGUI
    participant Storage as localStorage
    participant Doc as Document/Element
    participant Render as NetJSONGraphRender
    participant Leaflet as Leaflet Map

    Note over GUI,Render: Initialization
    User->>GUI: init()
    GUI->>GUI: createThemeControl()
    GUI->>Storage: read "map_theme"
    Storage-->>GUI: theme value
    GUI->>Doc: add/remove "dark-mode" class
    GUI-->>User: show theme control

    Note over User,GUI: Toggle theme
    User->>GUI: click theme toggle
    GUI->>Doc: toggle "dark-mode" class
    GUI->>Storage: write "map_theme"
    GUI->>Render: trigger re-render()

    Note over Render,Leaflet: Tile selection and update
    Render->>Render: isDarkMode(self) checks Doc/element
    alt Dark mode
        Render->>Render: select mapTileConfigDark
    else Light mode
        Render->>Render: select mapTileConfig
    end
    Render->>Leaflet: remove old TileLayer(s) and add selected TileLayer(s)
    Leaflet-->>User: render updated tiles
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

javascript

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Implement dark mode support for map and UI' is clear, concise, and accurately summarizes the main feature addition across the codebase.
Description check ✅ Passed The PR description comprehensively covers key changes, implementation details, testing approach, and references the linked issue #475, fulfilling all required template sections.
Linked Issues check ✅ Passed The PR implementation fully addresses issue #475 objectives: dark-themed map tiles via CartoDB Dark Matter [#475], dark-mode UI styling [#475], persistent theme preference [#475], and comprehensive testing [#475].
Out of Scope Changes check ✅ Passed All changes directly support dark mode implementation: theme toggle UI, CSS styling, tile configuration, dark-mode detection utilities, and comprehensive test coverage with no unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

🧹 Recent nitpick comments
src/js/netjsongraph.render.js (1)

507-523: Consider extracting tile selection into a helper to reduce duplication.

The tile selection logic (lines 509-512) duplicates the pattern from generateMapOption (lines 411-414). While both locations need the tiles for different purposes (echarts option vs. manual Leaflet layer management), a small helper could reduce duplication:

♻️ Proposed helper

Add a helper method in the same class or utility:

getActiveTileConfig(self) {
  const isDarkMode = self.utils.isDarkMode(self);
  return isDarkMode && self.config.mapTileConfigDark
    ? self.config.mapTileConfigDark
    : self.config.mapTileConfig;
}

Then use it in both generateMapOption and mapRender:

-    const isDarkMode = self.utils.isDarkMode(self);
-    const targetTiles =
-      isDarkMode && self.config.mapTileConfigDark
-        ? self.config.mapTileConfigDark
-        : self.config.mapTileConfig;
+    const targetTiles = this.getActiveTileConfig(self);
📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 40d0512 and 1dbf153.

📒 Files selected for processing (7)
  • public/example_templates/netjsonmap.html
  • src/js/netjsongraph.config.js
  • src/js/netjsongraph.gui.js
  • src/js/netjsongraph.render.js
  • src/js/netjsongraph.util.js
  • test/netjsongraph.dom.test.js
  • test/netjsongraph.render.test.js
✅ Files skipped from review due to trivial changes (1)
  • public/example_templates/netjsonmap.html
🚧 Files skipped from review as they are similar to previous changes (5)
  • test/netjsongraph.dom.test.js
  • src/js/netjsongraph.util.js
  • src/js/netjsongraph.config.js
  • test/netjsongraph.render.test.js
  • src/js/netjsongraph.gui.js
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,ts,css,scss,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using openwisp-qa-format command with Python virtualenv enabled and yarn lint:fix (runs eslint --fix and prettier via lint-staged)

Files:

  • src/js/netjsongraph.render.js
src/**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

Husky pre-commit hooks automatically run lint-staged to format JavaScript files in src/**/*.js with prettier

Files:

  • src/js/netjsongraph.render.js
🧠 Learnings (2)
📚 Learning: 2026-01-20T00:56:36.062Z
Learnt from: dee077
Repo: openwisp/netjsongraph.js PR: 417
File: src/js/netjsongraph.core.js:132-145
Timestamp: 2026-01-20T00:56:36.062Z
Learning: In the netjsongraph.js codebase, the data model does not support parallel links (multiple links between the same source and target). Ensure nodeLinkIndex is keyed by a single 'source~target' pair and that adding a link does not overwrite existing links for that pair. If parallel links are ever required, the data model must be changed (e.g., allow an array of links per 'source~target' key) rather than storing only one link.

Applied to files:

  • src/js/netjsongraph.render.js
📚 Learning: 2026-01-20T16:50:56.414Z
Learnt from: codesankalp
Repo: openwisp/netjsongraph.js PR: 425
File: src/js/netjsongraph.render.js:1-26
Timestamp: 2026-01-20T16:50:56.414Z
Learning: In netjsongraph.js, prefer importing from echarts/lib/chart/*/install, echarts/lib/component/*/install, and echarts/lib/renderer/install* rather than the public entry points (echarts/charts, echarts/components, echarts/renderers) to improve tree-shaking and reduce bundle size. Apply this pattern across JS files in src (update imports accordingly) and verify by comparing bundle sizes between the install-path imports and the public-entry imports.

Applied to files:

  • src/js/netjsongraph.render.js
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Tests and Coverage
🔇 Additional comments (5)
src/js/netjsongraph.render.js (5)

410-418: Good centralization of dark mode detection.

The use of self.utils.isDarkMode(self) properly addresses the previous review concern about duplicate dark mode detection logic. The conditional tile selection correctly falls back to mapTileConfig when mapTileConfigDark is not defined.


572-636: LGTM!

The Leaflet guards are appropriate defensive coding. The zoomend handler correctly:

  • Toggles label visibility based on zoom level
  • Handles float zoom levels with Math.round() for zoom control state management

640-691: LGTM!

The Leaflet guard around the moveend handler is appropriate. The data loading and bounds management logic is correctly preserved within the conditional.


724-731: LGTM!

The defensive guards with fallback values are well-implemented. The fallbacks (0 for current zoom, 18 for max zoom) are sensible defaults that prevent errors when Leaflet is unavailable while maintaining reasonable behavior.


736-760: LGTM!

The clustering zoomend handler is properly guarded. The logic correctly handles the transition between clustered and unclustered views based on the disableClusteringAtLevel threshold.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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.

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: 3

🤖 Fix all issues with AI Agents
In @src/js/netjsongraph.gui.js:
- Around line 54-56: Replace the truthy-function check with a comparison against
the active render mode: instead of testing `this.self.utils.mapRender` directly,
verify `this.self.config.render === this.self.utils.mapRender` and only then
call `this.self.utils.render()` so re-rendering occurs only when map mode is
active; update the conditional around `this.self.utils.render` accordingly.

In @src/js/netjsongraph.render.js:
- Around line 401-407: Extract the duplicated dark-mode detection into a single
helper method (e.g., isDarkMode()) on the same renderer class used in this file;
implement it to consistently check document.body.classList,
document.documentElement.classList, and self.el (if present) for "dark-mode" and
return a boolean, then replace both inline checks (the const isDarkMode = ...
block around lines ~401 and the similar block around lines ~498) with calls to
this.new helper to remove duplication and ensure identical behavior.

In @test/netjsongraph.render.test.js:
- Around line 945-947: The mock Leaflet object defines addLayer twice (duplicate
of addLayer alongside eachLayer and removeLayer); remove the redundant addLayer
entry so the mock only declares addLayer once (keep the intended
implementation), update any references if needed, and run the tests to confirm
no behavioral change from the deduped mock in netjsongraph.render.test.js.
🧹 Nitpick comments (3)
src/js/netjsongraph.render.js (3)

469-471: Validate dark mode tile config to improve error messaging.

The validation at line 469 only checks mapTileConfig[0], but when dark mode is active, the code uses mapTileConfigDark if available. If a developer only configures mapTileConfigDark and enables dark mode by default, they'll get a misleading error about mapTileConfig.

🔎 Proposed validation enhancement
-    if (!self.config.mapTileConfig[0]) {
-      throw new Error(`You must add the tiles via the "mapTileConfig" param!`);
+    const isDarkMode = this.isDarkModeActive(self);
+    const activeTileConfig = isDarkMode && self.config.mapTileConfigDark 
+      ? self.config.mapTileConfigDark 
+      : self.config.mapTileConfig;
+    
+    if (!activeTileConfig || !activeTileConfig[0]) {
+      const configName = isDarkMode && self.config.mapTileConfigDark 
+        ? "mapTileConfigDark" 
+        : "mapTileConfig";
+      throw new Error(`You must add the tiles via the "${configName}" param!`);
     }

508-518: Manual tile layer management may cause visual flicker during theme toggle.

The approach of removing all TileLayer instances and re-adding them (lines 509-517) is a workaround for echarts-leaflet not updating tiles on setOption. This can cause a brief flash where no tiles are visible during the transition.

Consider these alternatives:

  1. Check if the current tile layer URL matches the target before removing/re-adding
  2. Add new layers before removing old ones to maintain continuous coverage
  3. Investigate if echarts-leaflet has a supported API for updating tile configurations
🔎 Alternative approach to minimize flicker
     if (self.leaflet) {
+      // Collect existing tile layers
+      const existingLayers = [];
       self.leaflet.eachLayer((layer) => {
         if (layer instanceof L.TileLayer) {
-          self.leaflet.removeLayer(layer);
+          existingLayers.push(layer);
         }
       });
 
+      // Add new tile layers first
       targetTiles.forEach((tile) => {
         L.tileLayer(tile.urlTemplate, tile.options).addTo(self.leaflet);
       });
+      
+      // Then remove old layers to maintain continuous coverage
+      existingLayers.forEach(layer => {
+        self.leaflet.removeLayer(layer);
+      });
     }

493-495: Extensive Leaflet guards in map rendering function.

The code adds numerous guards checking if (self.leaflet) before accessing Leaflet methods throughout the mapRender function. While defensive programming is good, this function inherently requires Leaflet to work (map tiles, coordinates, zoom, etc.).

The extensive guards suggest one of two scenarios:

  1. Leaflet might not be initialized at certain points during the render lifecycle
  2. The code is overly defensive for a dependency that should always be present

If Leaflet can legitimately be absent at these points, consider failing fast at the beginning of mapRender with a clear error. If these guards are purely defensive against edge cases, they're well-implemented but may indicate an architectural issue worth investigating.

Also applies to: 589-630, 634-685, 693-754

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e503883 and c26f611.

📒 Files selected for processing (8)
  • public/example_templates/netjsonmap.html
  • src/css/netjsongraph-theme.css
  • src/js/netjsongraph.config.js
  • src/js/netjsongraph.gui.js
  • src/js/netjsongraph.render.js
  • test/darkmode.test.js
  • test/netjsongraph.dom.test.js
  • test/netjsongraph.render.test.js
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-04T08:00:40.936Z
Learnt from: dee077
Repo: openwisp/netjsongraph.js PR: 417
File: src/js/netjsongraph.util.js:1302-1341
Timestamp: 2026-01-04T08:00:40.936Z
Learning: In the netjsongraph.js codebase, the `== null` pattern is intentionally used to check for both null and undefined values in a single comparison, which is a well-established JavaScript idiom and should not be flagged for replacement with strict equality checks.

Applied to files:

  • test/netjsongraph.dom.test.js
🧬 Code graph analysis (1)
test/darkmode.test.js (2)
test/netjsongraph.render.test.js (3)
  • JSONData (5-9)
  • graph (36-36)
  • graph (299-299)
test/netjsongraph.dom.test.js (1)
  • graph (5-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Tests and Coverage
🔇 Additional comments (6)
test/netjsongraph.dom.test.js (1)

306-307: LGTM!

The test expectations correctly validate that the GUI initialization now creates a controls container and includes the new theme control element. This aligns well with the dark mode feature implementation.

test/darkmode.test.js (2)

13-33: Test setup looks reasonable.

The beforeEach setup correctly initializes the graph and render instances with mock tile configurations. The assignment of render to graph.utils (line 32) enables the tests to call render.generateMapOption with the graph context, which is appropriate for isolated unit testing of the render logic.


35-72: Comprehensive dark mode test coverage.

The test suite effectively validates all key scenarios:

  • Default light mode behavior
  • Dark mode activation via body or html element classes
  • Graceful fallback when dark configuration is absent

Each test properly cleans up DOM state, preventing side effects between tests.

test/netjsongraph.render.test.js (1)

500-502: Test mocks properly updated for new tile configuration structure.

The updates to mapTileConfig and Leaflet mock methods correctly align test scaffolding with the new dark mode tile configuration structure. The consistent use of the OpenStreetMap tile URL template with proper placeholder syntax ensures tests remain valid.

Also applies to: 968-970, 1070-1072, 1177-1179, 1214-1216, 1387-1389, 1439-1441, 1449-1451

src/js/netjsongraph.config.js (1)

260-269: CartoDB tile configuration is valid for production.

The Dark Matter tiles from basemaps.cartocdn.com are publicly available and do not require API keys. The URL endpoint is accessible, the attribution is correct, and the subdomain distribution is properly configured.

src/css/netjsongraph-theme.css (1)

91-157: Dark mode styles are comprehensive and well-structured.

The dark mode implementation covers all necessary UI elements (sidebar, tooltips, search button, switches, theme control) with appropriate color choices and includes responsive behavior for small screens. The styling is clean and follows a consistent dark theme pattern.

Comment on lines 401 to 407
const isDarkMode =
document.body.classList.contains("dark-mode") ||
document.documentElement.classList.contains("dark-mode");
const tiles =
isDarkMode && configs.mapTileConfigDark
? configs.mapTileConfigDark
: configs.mapTileConfig;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Duplicate dark mode detection logic with inconsistent element checks.

Dark mode detection is duplicated in two places with slightly different implementations:

  • Lines 401-403 check document.body and document.documentElement
  • Lines 498-501 additionally check self.el

This duplication creates maintenance burden and the inconsistency could lead to different behavior in edge cases.

🔎 Proposed refactor to extract dark mode detection

Add a helper method to the class:

+  isDarkModeActive(self) {
+    return (
+      (self.el && self.el.classList.contains("dark-mode")) ||
+      document.body.classList.contains("dark-mode") ||
+      document.documentElement.classList.contains("dark-mode")
+    );
+  }
+
   generateMapOption(JSONData, self, clusters = []) {
     // ... existing code ...
     
-    const isDarkMode =
-      document.body.classList.contains("dark-mode") ||
-      document.documentElement.classList.contains("dark-mode");
+    const isDarkMode = this.isDarkModeActive(self);
     const tiles =
       isDarkMode && configs.mapTileConfigDark
         ? configs.mapTileConfigDark
         : configs.mapTileConfig;

Then use the same helper at lines 498-506:

-    const isDarkMode =
-      (self.el && self.el.classList.contains("dark-mode")) ||
-      document.body.classList.contains("dark-mode") ||
-      document.documentElement.classList.contains("dark-mode");
+    const isDarkMode = this.isDarkModeActive(self);
     
     const targetTiles =
       isDarkMode && self.config.mapTileConfigDark
         ? self.config.mapTileConfigDark
         : self.config.mapTileConfig;

Also applies to: 498-506

🤖 Prompt for AI Agents
In @src/js/netjsongraph.render.js around lines 401 - 407, Extract the duplicated
dark-mode detection into a single helper method (e.g., isDarkMode()) on the same
renderer class used in this file; implement it to consistently check
document.body.classList, document.documentElement.classList, and self.el (if
present) for "dark-mode" and return a boolean, then replace both inline checks
(the const isDarkMode = ... block around lines ~401 and the similar block around
lines ~498) with calls to this.new helper to remove duplication and ensure
identical behavior.

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: 2

🤖 Fix all issues with AI agents
In @src/js/netjsongraph.gui.js:
- Around line 43-46: Wrap the localStorage access for theme in try-catch: when
reading the theme (the current localStorage.getItem("map_theme") usage that
assigns savedTheme and conditions calling
this.self.el.classList.add("dark-mode")) wrap the getItem in try/catch and treat
failures as no saved theme (don’t throw), and when persisting the theme (the
localStorage.setItem call around line 51) wrap setItem in try/catch and silently
ignore storage errors (optionally log to console.debug) so the UI initialization
and theme toggling (the savedTheme variable, localStorage.getItem,
localStorage.setItem, and this.self.el.classList.add calls) won’t break if
storage is disabled.
- Around line 32-34: The theme toggle icon span (variable "icon") currently uses
an emoji which may not be announced by screen readers; update the span to
improve accessibility by adding an appropriate ARIA attribute: if the emoji is
purely decorative set aria-hidden="true" and role="img" as needed, or provide an
explicit aria-label (e.g., aria-label="Toggle theme" or aria-hidden="true" if
decorative) so assistive tech gets the correct intent; locate the code that
creates the span ("const icon = document.createElement('span')", "icon.innerHTML
= '🌓'") and add the chosen ARIA attribute and/or role accordingly.
🧹 Nitpick comments (1)
src/js/netjsongraph.gui.js (1)

315-316: Consider making theme control creation configurable.

The comment suggests making the theme control configurable. While the current unconditional creation works, consider adding a config option (e.g., showThemeToggle: true) for consistency with other UI controls like switchMode and to allow users to hide it if they implement custom theme switching.

🔧 Proposed refactor for configurability

In your default config (likely in netjsongraph.config.js):

showThemeToggle: true,

Then update the init method:

   init() {
     this.sideBar = this.createSideBar();
-    // Always create theme control for now, or make it configurable
-    this.createThemeControl();
+    if (this.self.config.showThemeToggle) {
+      this.createThemeControl();
+    }
     
     if (this.self.config.switchMode) {
       if (!this.controls) {
          this.controls = this.createControls();
       }
       this.renderModeSelector = this.createRenderModeSelector();
     }
   }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c26f611 and d6e8179.

📒 Files selected for processing (3)
  • src/js/netjsongraph.gui.js
  • src/js/netjsongraph.render.js
  • test/netjsongraph.render.test.js
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-04T08:00:40.936Z
Learnt from: dee077
Repo: openwisp/netjsongraph.js PR: 417
File: src/js/netjsongraph.util.js:1302-1341
Timestamp: 2026-01-04T08:00:40.936Z
Learning: In the netjsongraph.js codebase, the `== null` pattern is intentionally used to check for both null and undefined values in a single comparison, which is a well-established JavaScript idiom and should not be flagged for replacement with strict equality checks.

Applied to files:

  • src/js/netjsongraph.gui.js
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Tests and Coverage
🔇 Additional comments (4)
test/netjsongraph.render.test.js (1)

500-502: LGTM! Test scaffolding properly updated for dark mode support.

The test mocks have been consistently updated to support the new dark-mode feature:

  • mapTileConfig now uses proper array structure with tile configuration objects
  • isDarkMode mocks added throughout to simulate theme-aware behavior
  • Leaflet instance mocks include eachLayer and removeLayer for tile layer management

All changes align with the dark-mode implementation in the production code.

Also applies to: 506-506, 946-948, 969-971, 981-981, 1072-1074, 1085-1085, 1217-1219, 1231-1231, 1291-1291, 1327-1327, 1393-1395, 1399-1399, 1446-1448, 1456-1458, 1470-1470

src/js/netjsongraph.render.js (3)

41-47: LGTM! Dark mode detection properly consolidated.

The isDarkMode(self) helper consolidates dark-mode detection logic into a single reusable method that checks the element, body, and document classes. This addresses the previous duplication concern and ensures consistent behavior throughout the codebase.


411-419: Dark-mode tile selection implemented correctly.

The dark-mode aware tile selection properly uses isDarkMode(self) to choose between mapTileConfigDark and mapTileConfig. The manual tile layer management in mapRender (lines 506-523) is a necessary workaround because echarts-leaflet doesn't automatically handle tile updates on setOption. The implementation correctly:

  • Removes existing TileLayer instances
  • Adds new tiles based on current dark mode state
  • Guards all operations with Leaflet availability checks

Also applies to: 506-523


501-503: LGTM! Leaflet operations properly guarded.

All Leaflet-dependent operations are now correctly guarded with if (self.leaflet) checks, preventing runtime errors when Leaflet is unavailable. This defensive programming approach ensures the code gracefully handles mixed rendering environments while preserving existing behavior when Leaflet is present.

Key improvements:

  • Tile layer management guarded (lines 513-523)
  • Bounds fitting guarded (line 571)
  • Zoom-based label toggling guarded (lines 576-592, 594-635)
  • Move-end data fetching guarded (lines 639-690)
  • Clustering zoom handlers guarded (lines 698-759)

Also applies to: 513-523, 571-573, 576-592, 594-635, 639-690, 698-759

…ve accessibility, and add error handling

- Refactor isDarkMode() to NetJSONGraphUtil to eliminate code duplication
- Add aria-label to theme toggle icon for better screen reader support
- Wrap localStorage access in try-catch blocks to handle SecurityError
- Fix malformed JSDoc comments in netjsongraph.render.js

Addresses CodeRabbit review comments in PR openwisp#483
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: 1

🤖 Fix all issues with AI agents
In `@src/js/netjsongraph.gui.js`:
- Around line 43-44: Remove the duplicated comment line "// Check initial state"
in src/js/netjsongraph.gui.js so only one instance remains; locate the repeated
comment near the top of the GUI initialization (around the initial state check)
and delete the redundant line, leaving a single "// Check initial state"
comment.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6e8179 and 54c02f2.

📒 Files selected for processing (3)
  • src/js/netjsongraph.gui.js
  • src/js/netjsongraph.render.js
  • src/js/netjsongraph.util.js
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-01-04T08:00:40.936Z
Learnt from: dee077
Repo: openwisp/netjsongraph.js PR: 417
File: src/js/netjsongraph.util.js:1302-1341
Timestamp: 2026-01-04T08:00:40.936Z
Learning: In the netjsongraph.js codebase, the `== null` pattern is intentionally used to check for both null and undefined values in a single comparison, which is a well-established JavaScript idiom and should not be flagged for replacement with strict equality checks.

Applied to files:

  • src/js/netjsongraph.gui.js
📚 Learning: 2026-01-20T00:56:36.062Z
Learnt from: dee077
Repo: openwisp/netjsongraph.js PR: 417
File: src/js/netjsongraph.core.js:132-145
Timestamp: 2026-01-20T00:56:36.062Z
Learning: In the netjsongraph.js codebase, the data model does not support parallel links (multiple links between the same source and target). Ensure nodeLinkIndex is keyed by a single 'source~target' pair and that adding a link does not overwrite existing links for that pair. If parallel links are ever required, the data model must be changed (e.g., allow an array of links per 'source~target' key) rather than storing only one link.

Applied to files:

  • src/js/netjsongraph.gui.js
  • src/js/netjsongraph.render.js
  • src/js/netjsongraph.util.js
📚 Learning: 2026-01-04T08:00:36.595Z
Learnt from: dee077
Repo: openwisp/netjsongraph.js PR: 417
File: src/js/netjsongraph.util.js:1302-1341
Timestamp: 2026-01-04T08:00:36.595Z
Learning: In the netjsongraph.js codebase, specifically in src/js/netjsongraph.util.js, the pattern '== null' is intentionally used to detect both null and undefined in a single comparison. Do not flag or replace these checks with strict equality checks (=== null or === undefined) for this file; preserve the established idiom.

Applied to files:

  • src/js/netjsongraph.util.js
🧬 Code graph analysis (1)
src/js/netjsongraph.util.js (1)
test/netjsongraph.render.test.js (1)
  • self (484-484)
🪛 GitHub Actions: netjsongraph.js CI BUILD
src/js/netjsongraph.gui.js

[error] 1-1: Prettier formatting check failed. Run 'Prettier --write' (or the project format script) to fix code style issues.

src/js/netjsongraph.render.js

[error] 411-411: TypeError: self.utils.isDarkMode is not a function

🔇 Additional comments (10)
src/js/netjsongraph.util.js (1)

255-271: LGTM!

The isDarkMode helper correctly centralizes dark mode detection by checking all relevant DOM locations (self.el, document.body, document.documentElement) with proper null-safety on self.el.

src/js/netjsongraph.render.js (5)

491-513: Leaflet guards and tile management look correct, but same isDarkMode issue.

The defensive guards around self.leaflet (lines 491-493, 503-512) are appropriate to prevent runtime errors when Leaflet is unavailable. The tile layer swap logic correctly removes existing TileLayer instances before adding new ones based on the theme.

However, line 496 has the same self.utils.isDarkMode(self) call that will fail. Once the method accessibility is fixed (per the earlier comment), this code will work correctly.


561-625: LGTM!

The Leaflet guards throughout this section are well-placed defensive measures. The zoom-end handler correctly manages label visibility based on the zoom threshold, and the manual zoom control state management (lines 611-623) appropriately handles edge cases with float zoom levels using Math.round().


629-680: LGTM!

The moveend handler is properly guarded with a Leaflet existence check. The internal logic for fetching and merging bbox data remains correct.


713-720: LGTM!

The cluster click handler uses appropriate guards and fallback values when Leaflet is unavailable. The setView call is correctly wrapped in a conditional check.


401-405: Manual verification required for isDarkMode availability.

The isDarkMode function could not be found in the public netjsongraph.js repository. Verify that:

  1. The function is defined and exported from NetJSONGraphUtil
  2. self.utils is properly composed to include methods from NetJSONGraphUtil
  3. The method is accessible at the call site (lines 401-405)

If the TypeError occurs in your build, trace where self.utils is assigned and confirm isDarkMode is included in that composition.

src/js/netjsongraph.gui.js (4)

32-35: Good accessibility improvement.

The aria-label attribute on the icon element properly addresses screen reader accessibility for the emoji-based toggle icon.


45-52: Good defensive error handling for localStorage.

The try-catch blocks around localStorage.getItem and localStorage.setItem appropriately handle scenarios where storage is unavailable (private browsing, disabled storage), ensuring the theme toggle doesn't break initialization.

Also applies to: 57-61


323-334: LGTM!

The init() method correctly creates the theme control unconditionally after the sidebar, and properly guards against overwriting existing controls when switchMode is enabled.


28-78: No action needed. The code already passes Prettier formatting checks without any issues.

Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

- Fixed test setup to include isDarkMode method in graph.utils
- All dark mode tests now pass successfully
- Removed unnecessary NetJSONGraphUtil import in test file
- Simplified arrow function to use implicit return
- Fixes QA-Checks CI failure
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: 1

🤖 Fix all issues with AI agents
In `@test/darkmode.test.js`:
- Around line 71-79: In the test "Should fallback to light tiles if dark config
is missing" add an assertion that option.leaflet.tiles.urlTemplate equals the
light config's template to match the other tests; specifically, after the
existing expect(option.leaflet.tiles).toEqual(graph.config.mapTileConfig) add an
assertion comparing option.leaflet.tiles.urlTemplate to
graph.config.mapTileConfig.urlTemplate so the test explicitly verifies the URL
template fallback.
🧹 Nitpick comments (1)
test/darkmode.test.js (1)

42-79: Consider using afterEach for test cleanup.

The current cleanup pattern is fragile—if an assertion fails before the manual classList.remove() calls, subsequent tests could be affected by leftover state.

♻️ Suggested refactor using afterEach
   });
+
+  afterEach(() => {
+    document.body.classList.remove("dark-mode");
+    document.documentElement.classList.remove("dark-mode");
+  });

   test("Should use light tiles by default", () => {
-    document.body.classList.remove("dark-mode");
-    document.documentElement.classList.remove("dark-mode");
-    
     const option = render.generateMapOption(JSONData, graph);
     expect(option.leaflet.tiles).toEqual(graph.config.mapTileConfig);
     expect(option.leaflet.tiles[0].urlTemplate).toBe("light-tiles");
   });

   test("Should use dark tiles when body has dark-mode class", () => {
     document.body.classList.add("dark-mode");
     
     const option = render.generateMapOption(JSONData, graph);
     expect(option.leaflet.tiles).toEqual(graph.config.mapTileConfigDark);
     expect(option.leaflet.tiles[0].urlTemplate).toBe("dark-tiles");
-    
-    document.body.classList.remove("dark-mode");
   });

   test("Should use dark tiles when html has dark-mode class", () => {
     document.documentElement.classList.add("dark-mode");
     
     const option = render.generateMapOption(JSONData, graph);
     expect(option.leaflet.tiles).toEqual(graph.config.mapTileConfigDark);
     expect(option.leaflet.tiles[0].urlTemplate).toBe("dark-tiles");
-    
-    document.documentElement.classList.remove("dark-mode");
   });

   test("Should fallback to light tiles if dark config is missing", () => {
     document.body.classList.add("dark-mode");
     delete graph.config.mapTileConfigDark;
     
     const option = render.generateMapOption(JSONData, graph);
     expect(option.leaflet.tiles).toEqual(graph.config.mapTileConfig);
-    
-    document.body.classList.remove("dark-mode");
   });
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c5c0a18 and 40d0512.

📒 Files selected for processing (1)
  • test/darkmode.test.js
🧰 Additional context used
🧬 Code graph analysis (1)
test/darkmode.test.js (2)
test/netjsongraph.render.test.js (3)
  • JSONData (5-9)
  • graph (36-36)
  • graph (299-299)
test/netjsongraph.dom.test.js (1)
  • graph (5-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Tests and Coverage
🔇 Additional comments (2)
test/darkmode.test.js (2)

1-7: LGTM!

Imports and test data setup are appropriate and consistent with patterns in other test files.


13-40: LGTM!

The test setup properly mocks the configuration and utilities needed to test dark mode tile selection logic in isolation.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +71 to +79
test("Should fallback to light tiles if dark config is missing", () => {
document.body.classList.add("dark-mode");
delete graph.config.mapTileConfigDark;

const option = render.generateMapOption(JSONData, graph);
expect(option.leaflet.tiles).toEqual(graph.config.mapTileConfig);

document.body.classList.remove("dark-mode");
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add urlTemplate assertion for consistency with other tests.

The fallback test verifies tiles equals the light config but doesn't explicitly check the urlTemplate value like the other three tests do. Adding this assertion improves consistency and makes the test more explicit.

💡 Suggested fix
   test("Should fallback to light tiles if dark config is missing", () => {
     document.body.classList.add("dark-mode");
     delete graph.config.mapTileConfigDark;
     
     const option = render.generateMapOption(JSONData, graph);
     expect(option.leaflet.tiles).toEqual(graph.config.mapTileConfig);
+    expect(option.leaflet.tiles[0].urlTemplate).toBe("light-tiles");
     
     document.body.classList.remove("dark-mode");
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("Should fallback to light tiles if dark config is missing", () => {
document.body.classList.add("dark-mode");
delete graph.config.mapTileConfigDark;
const option = render.generateMapOption(JSONData, graph);
expect(option.leaflet.tiles).toEqual(graph.config.mapTileConfig);
document.body.classList.remove("dark-mode");
});
test("Should fallback to light tiles if dark config is missing", () => {
document.body.classList.add("dark-mode");
delete graph.config.mapTileConfigDark;
const option = render.generateMapOption(JSONData, graph);
expect(option.leaflet.tiles).toEqual(graph.config.mapTileConfig);
expect(option.leaflet.tiles[0].urlTemplate).toBe("light-tiles");
document.body.classList.remove("dark-mode");
});
🤖 Prompt for AI Agents
In `@test/darkmode.test.js` around lines 71 - 79, In the test "Should fallback to
light tiles if dark config is missing" add an assertion that
option.leaflet.tiles.urlTemplate equals the light config's template to match the
other tests; specifically, after the existing
expect(option.leaflet.tiles).toEqual(graph.config.mapTileConfig) add an
assertion comparing option.leaflet.tiles.urlTemplate to
graph.config.mapTileConfig.urlTemplate so the test explicitly verifies the URL
template fallback.

@coderabbitai coderabbitai bot added the javascript Pull requests that update Javascript code label Jan 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

javascript Pull requests that update Javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Dark Mode for Map (Leaflet) and Info Labels

1 participant