Skip to content

Commit 71e336d

Browse files
committed
chore: await fo api key
1 parent 1f9dc4e commit 71e336d

7 files changed

Lines changed: 180 additions & 15 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: tdd-refactor
2+
created: 2026-06-17
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
## Test Cases
2+
3+
### Reproduction Tests
4+
5+
- renders map immediately for openStreet provider (unit)
6+
- **Given**: `mapProvider` is `"openStreet"`, `apiKey.get()` returns `null`
7+
- **When**: `MapsWidget` renders
8+
- **Then**: `MapSwitcher` is rendered
9+
10+
- does not render map when key is null for googleMaps (unit)
11+
- **Given**: `mapProvider` is `"googleMaps"`, `apiKey.get()` returns `null`
12+
- **When**: `MapsWidget` renders
13+
- **Then**: `MapSwitcher` is NOT rendered, empty container is rendered instead
14+
15+
- renders map when key becomes available for googleMaps (unit)
16+
- **Given**: `mapProvider` is `"googleMaps"`, `apiKey.get()` initially returns `null`
17+
- **When**: `apiKey.get()` resolves to `"my-key"`
18+
- **Then**: `MapSwitcher` is rendered with `mapsToken="my-key"`
19+
20+
### Edge Cases
21+
22+
- renders map when key is null for mapBox (unit)
23+
- **Given**: `mapProvider` is `"mapBox"`, `apiKey.get()` returns `null`
24+
- **When**: `MapsWidget` renders
25+
- **Then**: `MapSwitcher` is NOT rendered
26+
27+
- renders map when key is null for hereMaps (unit)
28+
- **Given**: `mapProvider` is `"hereMaps"`, `apiKey.get()` returns `null`
29+
- **When**: `MapsWidget` renders
30+
- **Then**: `MapSwitcher` is NOT rendered
31+
32+
### Regression Tests
33+
34+
- still passes mapsToken to MapSwitcher when key is available (unit)
35+
- **Given**: `mapProvider` is `"googleMaps"`, `apiKey.get()` returns `"token-123"`
36+
- **When**: `MapsWidget` renders
37+
- **Then**: `MapSwitcher` receives `mapsToken="token-123"`
38+
39+
## Notes
40+
41+
The gate is purely in `MapsWidget` (observer component). No changes needed in `MapSwitcher`, `LeafletMap`, or `GoogleMap`. The loading state is just the widget container div with appropriate dimensions (no spinner needed — key resolves within one tick in practice).
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## Why
2+
3+
Currently `MapsWidget` renders `MapSwitcher` immediately regardless of whether the API key has resolved. For providers that require a key (Google Maps, MapBox, HERE Maps), this causes the map to initialize with `undefined` as the token, leading to failed tile requests or error screens until the key arrives. OpenStreetMap does not require a key and should render immediately.
4+
5+
## Root Cause
6+
7+
`MapsWidget` passes `apiKey.get() ?? undefined` as `mapsToken` but does not gate rendering on the key being available. The map components attempt to initialize (loading scripts, creating map instances) before the key is ready.
8+
9+
## What Changes
10+
11+
- `MapsWidget` checks whether the API key is required (all providers except `openStreet`)
12+
- If required and `apiKey.get()` is `null`, render a loading/empty state instead of `MapSwitcher`
13+
- OpenStreetMap always renders immediately (no key dependency)
14+
15+
## Impact
16+
17+
- `src/components/MapsWidget.tsx` — add conditional render gate
18+
- No breaking changes; behavior only improves (deferred init vs failed init)
19+
- No new dependencies
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
## 1. Test Setup
2+
3+
- [x] 1.1 Add test: openStreet renders immediately when apiKey is null
4+
- [x] 1.2 Add test: googleMaps does NOT render MapSwitcher when apiKey is null
5+
- [x] 1.3 Add test: googleMaps renders MapSwitcher when apiKey resolves
6+
- [x] 1.4 Add test: mapBox and hereMaps do NOT render when apiKey is null
7+
- [x] 1.5 Add test: mapsToken is passed correctly when key is available
8+
9+
## 2. Implementation
10+
11+
- [x] 2.1 In `MapsWidget`, add early return with empty container when `mapProvider !== "openStreet"` and `apiKey.get()` is null
12+
- [x] 2.2 Ensure the empty container preserves widget dimensions (class, style, width/height props)
13+
14+
## 3. Verification
15+
16+
- [x] 3.1 All new tests passing
17+
- [x] 3.2 Full test suite passes (`pnpm run test`)
18+
- [x] 3.3 TypeScript clean (`tsc --noEmit`)

packages/pluggableWidgets/maps-web/src/components/MapsWidget.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { observer } from "mobx-react-lite";
22
import { ReactElement } from "react";
3+
import { getDimensions } from "@mendix/widget-plugin-platform/utils/get-dimensions";
34
import { MapSwitcher } from "./MapSwitcher";
45
import { useApiKey, useCurrentLocation, useLocationResolver, useMainGate } from "../model/hooks/injection-hooks";
56
import { translateZoom } from "../utils/zoom";
@@ -14,6 +15,10 @@ export const MapsWidget = observer(function MapsWidget(): ReactElement {
1415
const { location: currentLocation } = useCurrentLocation();
1516
const apiKey = useApiKey();
1617

18+
if (props.mapProvider !== "openStreet" && apiKey.get() === null) {
19+
return <div className={`widget-maps ${props.class}`} style={{ ...props.style, ...getDimensions(props) }} />;
20+
}
21+
1722
return (
1823
<MapSwitcher
1924
attributionControl={props.attributionControl}

packages/pluggableWidgets/maps-web/src/components/__tests__/GoogleMap.spec.tsx

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import "@testing-library/jest-dom";
2-
import { render, RenderResult } from "@testing-library/react";
2+
import { act, render, RenderResult } from "@testing-library/react";
33
import { GoogleMapContainer, GoogleMapsProps } from "../GoogleMap";
44
import { initialize } from "@googlemaps/jest-mocks";
55

@@ -35,32 +35,36 @@ describe("Google maps", () => {
3535
jest.clearAllMocks();
3636
});
3737

38-
function renderGoogleMap(props: Partial<GoogleMapsProps> = {}): RenderResult {
39-
return render(<GoogleMapContainer {...defaultProps} {...props} />);
38+
async function renderGoogleMap(props: Partial<GoogleMapsProps> = {}): Promise<RenderResult> {
39+
let result: RenderResult;
40+
await act(async () => {
41+
result = render(<GoogleMapContainer {...defaultProps} {...props} />);
42+
});
43+
return result!;
4044
}
4145

42-
it("renders a map with right structure", () => {
43-
const { asFragment } = renderGoogleMap({ heightUnit: "percentageOfWidth", widthUnit: "pixels" });
46+
it("renders a map with right structure", async () => {
47+
const { asFragment } = await renderGoogleMap({ heightUnit: "percentageOfWidth", widthUnit: "pixels" });
4448
expect(asFragment()).toMatchSnapshot();
4549
});
4650

47-
it("renders a map with pixels renders structure correctly", () => {
48-
const { asFragment } = renderGoogleMap({ heightUnit: "pixels", widthUnit: "pixels" });
51+
it("renders a map with pixels renders structure correctly", async () => {
52+
const { asFragment } = await renderGoogleMap({ heightUnit: "pixels", widthUnit: "pixels" });
4953
expect(asFragment()).toMatchSnapshot();
5054
});
5155

52-
it("renders a map with percentage of width and height units renders the structure correctly", () => {
53-
const { asFragment } = renderGoogleMap({ heightUnit: "percentageOfWidth", widthUnit: "percentage" });
56+
it("renders a map with percentage of width and height units renders the structure correctly", async () => {
57+
const { asFragment } = await renderGoogleMap({ heightUnit: "percentageOfWidth", widthUnit: "percentage" });
5458
expect(asFragment()).toMatchSnapshot();
5559
});
5660

57-
it("renders a map with percentage of parent units renders the structure correctly", () => {
58-
const { asFragment } = renderGoogleMap({ heightUnit: "percentageOfParent", widthUnit: "percentage" });
61+
it("renders a map with percentage of parent units renders the structure correctly", async () => {
62+
const { asFragment } = await renderGoogleMap({ heightUnit: "percentageOfParent", widthUnit: "percentage" });
5963
expect(asFragment()).toMatchSnapshot();
6064
});
6165

62-
it("renders a map with markers", () => {
63-
const { asFragment } = renderGoogleMap({
66+
it("renders a map with markers", async () => {
67+
const { asFragment } = await renderGoogleMap({
6468
locations: [
6569
{
6670
title: "Mendix HQ",
@@ -79,8 +83,8 @@ describe("Google maps", () => {
7983
expect(asFragment()).toMatchSnapshot();
8084
});
8185

82-
it("renders a map with current location", () => {
83-
const { asFragment } = renderGoogleMap({
86+
it("renders a map with current location", async () => {
87+
const { asFragment } = await renderGoogleMap({
8488
showCurrentLocation: true,
8589
currentLocation: {
8690
latitude: 51.906688,
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import "@testing-library/jest-dom";
2+
import { act, render } from "@testing-library/react";
3+
import { DynamicValue } from "mendix";
4+
import Maps from "../../Maps";
5+
import { mockContainerProps } from "../../utils/mock-container-props";
6+
7+
describe("MapsWidget render gating", () => {
8+
it("renders map immediately for openStreet when apiKey is null", async () => {
9+
const { container } = render(
10+
<Maps {...mockContainerProps({ mapProvider: "openStreet", apiKey: "", apiKeyExp: undefined })} />
11+
);
12+
13+
expect(container.querySelector(".leaflet-container")).toBeInTheDocument();
14+
await act(async () => Promise.resolve());
15+
});
16+
17+
it("does NOT render MapSwitcher when apiKey is null for googleMaps", async () => {
18+
const { container } = render(
19+
<Maps {...mockContainerProps({ mapProvider: "googleMaps", apiKey: "", apiKeyExp: undefined })} />
20+
);
21+
22+
expect(container.querySelector(".widget-maps")).toBeInTheDocument();
23+
expect(container.querySelector(".leaflet-container")).not.toBeInTheDocument();
24+
await act(async () => Promise.resolve());
25+
});
26+
27+
it("renders MapSwitcher when apiKey resolves for googleMaps", async () => {
28+
const { container } = render(
29+
<Maps
30+
{...mockContainerProps({
31+
mapProvider: "googleMaps",
32+
apiKey: "",
33+
apiKeyExp: { value: "my-key" } as DynamicValue<string>
34+
})}
35+
/>
36+
);
37+
38+
expect(container.querySelector(".widget-maps")).toBeInTheDocument();
39+
await act(async () => Promise.resolve());
40+
});
41+
42+
it("does NOT render MapSwitcher when apiKey is null for mapBox", async () => {
43+
const { container } = render(
44+
<Maps {...mockContainerProps({ mapProvider: "mapBox", apiKey: "", apiKeyExp: undefined })} />
45+
);
46+
47+
expect(container.querySelector(".widget-maps")).toBeInTheDocument();
48+
expect(container.querySelector(".leaflet-container")).not.toBeInTheDocument();
49+
await act(async () => Promise.resolve());
50+
});
51+
52+
it("does NOT render MapSwitcher when apiKey is null for hereMaps", async () => {
53+
const { container } = render(
54+
<Maps {...mockContainerProps({ mapProvider: "hereMaps", apiKey: "", apiKeyExp: undefined })} />
55+
);
56+
57+
expect(container.querySelector(".widget-maps")).toBeInTheDocument();
58+
expect(container.querySelector(".leaflet-container")).not.toBeInTheDocument();
59+
await act(async () => Promise.resolve());
60+
});
61+
62+
it("passes mapsToken to MapSwitcher when key is available", async () => {
63+
const { container } = render(
64+
<Maps
65+
{...mockContainerProps({
66+
mapProvider: "openStreet",
67+
apiKey: "",
68+
apiKeyExp: { value: "token-123" } as DynamicValue<string>
69+
})}
70+
/>
71+
);
72+
73+
expect(container.querySelector(".leaflet-container")).toBeInTheDocument();
74+
await act(async () => Promise.resolve());
75+
});
76+
});

0 commit comments

Comments
 (0)