terra-draw icon indicating copy to clipboard operation
terra-draw copied to clipboard

[Feature Request] Ease testing custom modes by exposing config or store builders in some way

Open acorncom opened this issue 1 week ago • 0 comments

Is your feature request related to a problem? Please describe.

We've been building a custom subclass for TerraDrawLineStringMode to handle some functionality needed within a client app. As the changes grow, we'd love to be writing unit tests as well. I started by using https://github.com/JamesLMilner/terra-draw/blob/b69c2c93e1c3585a63539bd63fcc742911fcccb7/packages/terra-draw/src/modes/linestring/linestring.mode.spec.ts#L1033-L1080 as an example of the type of thing we need, but it currently assumes access to private types and apis that custom subclasses don't have easy access to.

As done in the linestring.mode.spec tests, I'd love to be able to mock a config (with internal GeoJSONStore) to setup proper test fixtures, but it's not exported from the main terra-draw module.

Current workaround (less than ideal):

// tests/helpers/terra-draw.ts
import { TerraDrawExtend } from 'terra-draw';

// Accessing GeoJSONStore through TerraDrawExtend here feels like we're reaching into private api
// in ways we may regret in the future ;-)
const { GeoJSONStore } = TerraDrawExtend;

export const MockModeConfig = (mode: string) => {
  return {
    mode,
    store: new GeoJSONStore<OnChangeContext | undefined>(), // OnChangeContext is also a private type right now
    setCursor: createMockFn(),
    onChange: createMockFn(),
    // ... other config
  };
};

At the moment, in our tests, we're using the mock config to:

  • Create a mock mode configuration object that matches the TerraDrawModeRegisterConfig type
  • Pre-populate the store with test fixtures (e.g., existing geometries to snap to)
  • Assert on store state after mode interactions

Describe your proposed idea for the solution to this problem

Assuming you would like to be keeping GeoJSONStore as an implementation detail (vs making it public api), it would be lovely to expose some methods for use in tests to ease building the appropriate internal objects (and types). And it would make custom modes feel far safer to maintain as the library continues to grow and change.

Could see doing this in one of two ways:

  • export a store builder, exposeing just the bare minimum to get things running
  • export a config builder (that allows basic mocking) and a built, basic store

1. Store builder approach Requires consumers to handle filling the rest of the api contract for config, exposes consumers here to some breakage if/as this type changes but that's likely a reasonable risk for them to run.

// terra-draw internals
// all other mocking / stubbing has to be handled by the consumer
export function IncompleteConfigWithStore(config: unknown) {
  return {
    ...{
      store: new GeoJSONStore<TerraDrawOnChangeContext | undefined>(), // keeps this internal api for now
    }
    ...config,
  }
};

app side -

// tests/helpers/terra-draw.ts
import { IncompleteConfigWithStore } from 'terra-draw/test-support/store-for-config';

export const MockModeConfig = (mode: string) => {
  return IncompleteConfigWithStore{{
    mode,
    setCursor: createMockFn(), // mocking handled by consuming app as desired
    onChange: createMockFn(),
    // ... other config
  });
};
// tests/custom-linemode-test.js
import { MockModeConfig } from 'helpers/terra-draw';

it("creates the drag point when editable is true and a coordinate is selected", () => {
  const lineStringMode = new CustomLineMode({ editable: true });
  const mockConfig = MockModeConfig(lineStringMode.mode);
  lineStringMode.register(mockConfig);
  lineStringMode.start();

  // Create a linestring to edit
  lineStringMode.onClick(MockCursorEvent({ lng: 0, lat: 0 }));

  lineStringMode.onMouseMove(MockCursorEvent({ lng: 1, lat: 1 }));
});

2. Config builder approach We have two options here (either expose our existing Jest-based mocks - could export https://github.com/JamesLMilner/terra-draw/blob/main/packages/terra-draw/src/test/mock-mode-config.ts for instance) or fake out a light-weight config with the ability for consumers to override any desired methods. In the interests of not tying ourselves too tightly to Jest (and because the app I'm working on is running these tests browser-side via QUnit, so Jest isn't currently installed), the below shows the light-weight approach.

// terra-draw internals
// all other mocking / stubbing has to be handled by the consumer
const noop = () => {};

export function MockModeConfig(mode: string, mockOverrides = {}) {
  return {
    ...{
      store: new GeoJSONStore<TerraDrawOnChangeContext | undefined>(),
      setCursor: noop,
      onChange: noop,
      // ... other config
    }
    ...mockOverrides,
  }
};

app side -

// tests/custom-linemode-test.js
import { MockModeConfig } from 'terra-draw/test-support/mock-mode-config';

it("creates the drag point when editable is true and a coordinate is selected", () => {
  const lineStringMode = new CustomLineMode({ editable: true });
  const mockConfig = MockModeConfig(lineStringMode.mode);
  lineStringMode.register(mockConfig);
  lineStringMode.start();

  // Create a linestring to edit
  lineStringMode.onClick(MockCursorEvent({ lng: 0, lat: 0 }));

  lineStringMode.onMouseMove(MockCursorEvent({ lng: 1, lat: 1 }));
});

Describe alternatives you've considered

I could avoid using GeoJSONStore entirely and only test through full TerraDraw instances, but this makes it harder to:

  • Test edge cases in isolation
  • Mock specific store behaviors
  • Keep tests fast and focused

Interested in your thoughts, thanks! And happy to send in a PR for this if you're 👍 on us adding this

acorncom avatar Nov 17 '25 12:11 acorncom