Working Without Esri Watching
The map needs a WebGL context, a network, and a credential for a private portal. Unit tests have none of those, Storybook has only the first, and your laptop has all three but points at a WebMap with four cameras. This page is how the team works around each of those gaps without pretending they are not there: what to configure, what to mock, what was deliberately written to need no mocking, and how to look inside a running map when something is off.
Local configuration
Three settings decide which map you are looking at. ARCGIS_CLIENT_ID and ARCGIS_CLIENT_SECRET
in .env.local mint the token outside production; missing either makes /api/arcgis/token
throw "ArcGIS client credentials not configured", which you will see as a map stuck on its
loader. NEXT_PUBLIC_MASTER_MAP_PORTAL_ITEM_ID selects the WebMap, defaulting to the development
item; the UAT and beta WebMaps carry the full fleet and the Velocity sets, and are the ones to use
for anything about cameras at all — the development WebMap has no Velocity layers, so it draws
no cameras (one error line in the console says so). Because NEXT_PUBLIC_* values are inlined
at build time, a production build must be built with the environment file you intend to test
against; setting it at next start changes only server values.
The map's optional features sit behind flags from lib/featureFlags.ts, fetched from the backend
and overridden on localhost:
| Flag | Default | Localhost | Gates |
|---|---|---|---|
enable-camera-area-filter | off | on | Area scopes, map pick, popup action, scope bar |
enable-locate-on-map | off | on | Sightings on the map |
enable-what3words | on | on | The 3 m square and address surfaces |
enable-map-legend-button | unset (off) | unset | The Esri legend component |
enable-map-ruler-button | unset (off) | unset | The Esri distance measurement component |
enable-map-add-camera | unset (off) | unset | A video-call FAB in tile view |
A flag arrives after first render, so map hooks take it as an enabled input rather than being
skipped; turning a feature off mid-session tears its filters down instead of stranding them.
Anything about density, clustering, the arrow build, the extent query or the polling budget has to be tested against the UAT or beta WebMap and backend. A grid change that looked fine with four cameras collapsed the 1,200-camera beta grid once. Switch the environment file, rebuild, and look at a real fleet before calling a map change done.
Jest: mock the edges, test the middle
jest.config.js allows @arcgis/core through transformIgnorePatterns, so SDK modules can be
imported in tests, but a MapView cannot be constructed under jsdom and nobody tries. The pattern
is per-module mocks at the file boundary, shaped like the real thing:
jest.mock('@arcgis/core/core/reactiveUtils', () => ({
watch: jest.fn(() => ({ remove: jest.fn() })),
}));
jest.mock('@arcgis/core/geometry/Point', () => ({
__esModule: true,
default: class Point {
latitude: number;
longitude: number;
constructor(p: any) {
/* copy */
}
},
}));
CameraCallout.test.tsx and getBasemapByKey.test.ts are the reference examples. Note the
__esModule and default shape: the SDK's modules are default exports, and a plain
jest.fn() module mock gives you undefined where the class should be.
More valuable than good mocks is code that needs none. Several modules were written ArcGIS-free or structurally typed so their logic is testable as plain functions:
util/map/featureMap/velocityLayers.ts— discovery, ranking, companions, id-field resolution andcomposeVelocityWhereall run on plain objects withtitle,type,urlandfields. Its test file is the best model in the repo for testing map logic without a map.util/map/webMapLayerCatalog.ts— structuralCatalogLayerandCatalogCollectiontypes; a test builds collections from arrays.util/map/alertMapGeometry.ts— the email capture's framing, kept free of SDK imports on purpose.util/map/featureMap/areaFilterGeometry.ts— imports geometry operators, but its bounding-box prefilter andbuildCameraIdWhereClauseare pure.util/map/featureMap/cameraChangeDetector.ts— the PTZ diff, pure.
When you add logic to a hook, ask whether it can be a pure function in util/map/ first. The
hooks then become thin wiring that the existing hook tests cover with mocked stores.
Storybook: a mock map in the slot
The production WebMap is a private portal item behind token auth, so stories cannot show it.
.storybook/main.ts aliases context/MapPortalContext to a mock provider that renders a
self-contained SVG terrain map, clearly badged as a mock, into whichever slot a page registers.
Page-level stories therefore lay out correctly with no network. The store-isolation decorator
excludes the two ArcGIS-importing stores from eager snapshotting so they do not pull the SDK into
every story bundle. FeatureMap.stories.tsx has one LiveMap story that renders the real view
with Chromatic snapshots disabled; it exists to exercise the boot chrome, not the layers.
Looking inside a live map
The view is not on window. The quickest way in is the element:
const el = document.querySelector('arcgis-map');
const view = el.view; // MapView
view.map.allLayers.map(l => l.title); // what is ON the map (parked layers absent)
view.scale;
view.zoom;
view.extent.center; // where you are
view.map.allLayers.find(l => l.title === 'Locations').queryFeatureCount();
Parked layers are not in those collections; use the Layers Menu row state or the
[usePreparedWebMap] log line, which reports how many were catalogued, parked and kept. Redux
DevTools shows every map store under the ALERTCalifornia/ prefix, with ArcGIS objects replaced
by [ArcGIS Object] so the serializer does not choke on WebGL contexts. What discovery found of
the Velocity layers is under ALERTCalifornia/velocityLayersStore.
Two gotchas with automated browsers. A hidden browser pane fires no IntersectionObserver
callbacks and a hitTest never settles, so map interaction tests need a visible pane or a
screenshot first. And a headless Chromium needs software WebGL (--use-angle=swiftshader --enable-unsafe-swiftshader) to render the view at all; view.takeScreenshot() is the reliable
way to prove it did, since a screenshot of the page can show a black canvas while the view is fine.
Changing the catalog module under HMR
The layer catalog is registered in a module-level WeakMap. Fast Refresh of
webMapLayerCatalog.ts creates a new module instance with an empty registry while the live
WebMap still holds the old catalog, so finders return nothing until a full reload. If layers
"disappear" from the menu right after you edited that file, reload the page before debugging.
Wrapping up
Point your build at the fleet-sized WebMap before judging scale; the flags you need are on for
localhost. Mock the SDK at module boundaries with the right export shape, but prefer pure functions
in util/map/ that need no mock at all. Storybook swaps the provider for a mock terrain map. On a
live map, start from the element's view, remember parked layers are not in the collections, and
give headless browsers a software GPU.
Next: When the Map Is Wrong.