Skip to main content

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:

FlagDefaultLocalhostGates
enable-camera-area-filteroffonArea scopes, map pick, popup action, scope bar
enable-locate-on-mapoffonSightings on the map
enable-what3wordsononThe 3 m square and address surfaces
enable-map-legend-buttonunset (off)unsetThe Esri legend component
enable-map-ruler-buttonunset (off)unsetThe Esri distance measurement component
enable-map-add-cameraunset (off)unsetA 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.

Gotcha: the development backend has four cameras

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 and composeVelocityWhere all run on plain objects with title, type, url and fields. Its test file is the best model in the repo for testing map logic without a map.
  • util/map/webMapLayerCatalog.ts — structural CatalogLayer and CatalogCollection types; 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 and buildCameraIdWhereClause are 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.