Skip to content

Commit

Permalink
Add tests for basemap selector widget
Browse files Browse the repository at this point in the history
  • Loading branch information
sufyanAbbasi committed Nov 13, 2024
1 parent 41afc67 commit d26c584
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 54 deletions.
10 changes: 5 additions & 5 deletions geemap/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,13 +687,13 @@ def _layer_editor(self) -> Optional[map_widgets.LayerEditor]:
return self._find_widget_of_type(map_widgets.LayerEditor)

@property
def _basemap_selector(self) -> Optional[map_widgets.Basemap]:
def _basemap_selector(self) -> Optional[map_widgets.BasemapSelector]:
"""Finds the basemap selector widget in the map controls.
Returns:
Optional[map_widgets.Basemap]: The basemap selector widget if found, else None.
Optional[map_widgets.BasemapSelector]: The basemap selector widget if found, else None.
"""
return self._find_widget_of_type(map_widgets.Basemap)
return self._find_widget_of_type(map_widgets.BasemapSelector)

def __init__(self, **kwargs: Any) -> None:
"""Initialize the map with given keyword arguments.
Expand Down Expand Up @@ -1051,7 +1051,7 @@ def _add_basemap_selector(self, position: str, **kwargs: Any) -> None:
value = kwargs.pop(
"value", self._get_preferred_basemap_name(self.layers[0].name)
)
basemap = map_widgets.Basemap(basemap_names, value, **kwargs)
basemap = map_widgets.BasemapSelector(basemap_names, value, **kwargs)
basemap.on_close = lambda: self.remove("basemap_selector")
basemap.on_basemap_changed = self._replace_basemap
basemap_control = ipyleaflet.WidgetControl(widget=basemap, position=position)
Expand All @@ -1073,7 +1073,7 @@ def remove(self, widget: Any) -> None:
"layer_manager": map_widgets.LayerManager,
"layer_editor": map_widgets.LayerEditor,
"draw_control": MapDrawControl,
"basemap_selector": map_widgets.Basemap,
"basemap_selector": map_widgets.BasemapSelector,
}
if widget_type := basic_controls.get(widget, None):
if control := self._find_widget_of_type(widget_type, return_control=True):
Expand Down
10 changes: 5 additions & 5 deletions geemap/map_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1022,10 +1022,10 @@ def _observe_visible(self, change: Dict[str, Any]) -> None:


@Theme.apply
class Basemap(anywidget.AnyWidget):
class BasemapSelector(anywidget.AnyWidget):
"""Widget for selecting a basemap."""

_esm = pathlib.Path(__file__).parent / "static" / "basemap.js"
_esm = pathlib.Path(__file__).parent / "static" / "basemap_selector.js"

# The list of basemap names to make available for selection.
basemaps = traitlets.List([]).tag(sync=True)
Expand Down Expand Up @@ -1069,9 +1069,9 @@ def cleanup(self) -> None:
if self.on_close:
self.on_close()

def _on_close_click(self, _) -> None:
"""Handles the close button click event."""
self.cleanup()

# Type alias for backwards compatibility.
Basemap = BasemapSelector


@Theme.apply
Expand Down
30 changes: 20 additions & 10 deletions js/basemap_selector.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { RenderProps } from "@anywidget/types";
import { css, html, TemplateResult } from "lit";
import { property } from "lit/decorators.js";
import { css, html, PropertyValues, TemplateResult } from "lit";
import { property, query } from "lit/decorators.js";

import { legacyStyles } from "./ipywidgets_styles";
import { materialStyles } from "./styles";
Expand All @@ -24,17 +24,18 @@ export class BasemapSelector extends LitWidget<
legacyStyles,
materialStyles,
css`
.row {
.row-container {
align-items: center;
display: flex;
gap: 4px;
height: 30px;
gap: 2px;
height: 32px;
width: 200px;
}
.row-button {
font-size: 14px;
height: 26px;
width: 26px;
height: 28px;
width: 28px;
}
`,
];
Expand All @@ -51,11 +52,12 @@ export class BasemapSelector extends LitWidget<

@property({ type: Array }) basemaps: string[] = [];
@property({ type: String }) value: string = "";
@query('select') selectElement!: HTMLSelectElement;

render(): TemplateResult {
return html`
<div class="row">
<select @change=${this.onBaseMapChanged}>
<div class="row-container">
<select class="legacy-select" @change=${this.onChange}>
${this.basemaps.map((basemap) => html`<option>${basemap}</option>`)}
</select>
<button
Expand All @@ -66,8 +68,16 @@ export class BasemapSelector extends LitWidget<
</button>
</div>`;
}

override update(changedProperties: PropertyValues): void {
if (changedProperties.has("value") && this.selectElement) {
this.selectElement.value = this.value;
}
super.update(changedProperties);
}


private onBaseMapChanged(event: Event) {
private onChange(event: Event) {
const target = event.target as HTMLInputElement;
this.value = target.value;
}
Expand Down
25 changes: 25 additions & 0 deletions js/ipywidgets_styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,29 @@ export const legacyStyles = css`
height: var(--jp-widgets-inline-height);
line-height: var(--jp-widgets-inline-height);
}
.legacy-select {
padding-right: 20px;
border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);
border-radius: 0;
height: inherit;
flex: 1 1 var(--jp-widgets-inline-width-short);
min-width: 0;
box-sizing: border-box;
outline: none !important;
box-shadow: none;
background-color: var(--jp-widgets-input-background-color);
color: var(--jp-widgets-input-color);
font-size: var(--jp-widgets-font-size);
vertical-align: top;
padding-left: calc(var(--jp-widgets-input-padding)* 2);
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-repeat: no-repeat;
background-size: 20px;
background-position: right center;
background-image: var(--jp-widgets-dropdown-arrow);
}
}
`;
75 changes: 75 additions & 0 deletions tests/basemap_selector.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { AnyModel } from "@anywidget/types";
import "../js/basemap_selector";
import { default as selectorRender, BasemapSelector, BasemapSelectorModel } from "../js/basemap_selector";
import { FakeAnyModel } from "./fake_anywidget";

describe("<basemap-selector>", () => {
let selector: BasemapSelector;

async function makeSelector(model: AnyModel<BasemapSelectorModel>) {
const container = document.createElement("div");
selectorRender.render({
model, el: container, experimental: {
invoke: () => new Promise(() => [model, []]),
}
});
const element = container.firstElementChild as BasemapSelector;
document.body.appendChild(element);
await element.updateComplete;
return element;
}

beforeEach(async () => {
selector = await makeSelector(new FakeAnyModel<BasemapSelectorModel>({
basemaps: ["select", "default", "bounded"],
value: "default",
}));
});

afterEach(() => {
Array.from(document.querySelectorAll("basemap-selector")).forEach((el) => {
el.remove();
})
});

it("can be instantiated.", () => {
expect(selector.shadowRoot?.querySelector("select")?.textContent).toContain("bounded");
});

it("renders the basemap options.", () => {
const options = selector.shadowRoot?.querySelectorAll("option")!;
expect(options.length).toBe(3);
expect(options[0].textContent).toContain("select");
expect(options[1].textContent).toContain("default");
expect(options[2].textContent).toContain("bounded");
});

it("setting the value on model changes the value on select.", async () => {
selector.value = "select";
await selector.updateComplete;
expect(selector.selectElement.value).toBe("select");
});

it("sets value on model when option changes.", async () => {
const setSpy = spyOn(FakeAnyModel.prototype, "set");
const saveSpy = spyOn(FakeAnyModel.prototype, "save_changes");

selector.selectElement.value = "select";
selector.selectElement.dispatchEvent(new Event('change'));

await selector.updateComplete;
expect(setSpy).toHaveBeenCalledOnceWith("value", "select");
expect(saveSpy).toHaveBeenCalledTimes(1);
});

it("emits close event when clicked.", async () => {
const sendSpy = spyOn(FakeAnyModel.prototype, "send");
// Close button emits an event.
(selector.shadowRoot?.querySelector(".close-button") as HTMLButtonElement).click();
await selector.updateComplete;
expect(sendSpy).toHaveBeenCalledOnceWith({
type: "click",
id: "close"
});
});
});
49 changes: 15 additions & 34 deletions tests/test_map_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -638,51 +638,32 @@ def test_visibility_updates_children(self):
self.assertTrue(child.visible)


class TestBasemap(unittest.TestCase):
"""Tests for the Basemap class in the `map_widgets` module."""
class TestBasemapSelector(unittest.TestCase):
"""Tests for the BasemapSelector class in the `map_widgets` module."""

def setUp(self):
self.basemaps = ["first", "default", "bounded"]
self.default = "default"
self.basemap_widget = map_widgets.Basemap(self.basemaps, self.default)
self.basemap_widget = map_widgets.BasemapSelector(self.basemaps, self.default)

@property
def _close_button(self):
return utils.query_widget(
self.basemap_widget,
ipywidgets.Button,
lambda c: c.tooltip == "Close the basemap widget",
)

@property
def _dropdown(self):
return utils.query_widget(
self.basemap_widget, ipywidgets.Dropdown, lambda _: True
)

def test_basemap(self):
"""Tests that the basemap's initial UI is set up properly."""
self.assertIsNotNone(self._close_button)
self.assertIsNotNone(self._dropdown)
self.assertEqual(self._dropdown.value, "default")
self.assertEqual(len(self._dropdown.options), 3)
def test_basemap_default(self):
"""Tests that the default value is set."""
self.assertEqual(self.basemap_widget.value, "default")

def test_basemap_close(self):
"""Tests that triggering the closing button fires the close event."""
"""Tests that triggering the closing button fires the close callback."""
on_close_mock = Mock()
self.basemap_widget.on_close = on_close_mock
self._close_button.click()

msg = {"type": "click", "id": "close"}
self.basemap_widget._handle_custom_msg(msg, []) # pylint: disable=protected-access
on_close_mock.assert_called_once()

def test_basemap_selection(self):
"""Tests that a basemap selection fires the selected event."""
on_basemap_changed_mock = Mock()
self.basemap_widget.on_basemap_changed = on_basemap_changed_mock

self._dropdown.value = "first"

on_basemap_changed_mock.assert_called_once()
def test_basemap_change(self):
"""Tests that value change fires the basemap_changed callback."""
on_change_mock = Mock()
self.basemap_widget.on_basemap_changed = on_change_mock
self.basemap_widget.value = "ROADMAP"
on_change_mock.assert_called_once_with("ROADMAP")


class LayerEditorTestHarness:
Expand Down

0 comments on commit d26c584

Please sign in to comment.