Skip to content

Commit efc4a50

Browse files
authored
Merge pull request #390 from gtt-project/test/registries-and-eventbus
test: cover the geocoder/layer registries and the event bus
2 parents 9f65bb6 + 235bb0b commit efc4a50

3 files changed

Lines changed: 278 additions & 0 deletions

File tree

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
3+
import { GttEventBus } from './bus';
4+
import { GttEvent } from './types';
5+
6+
// Lifecycle payloads carry live client/map objects in production; tests only
7+
// check delivery, so a marker object cast to the payload type is enough.
8+
const payload = (tag: string) => ({ client: {}, map: {}, tag }) as any;
9+
10+
describe('GttEventBus', () => {
11+
describe('in-process subscribers', () => {
12+
it('delivers the payload to a subscriber', () => {
13+
const bus = new GttEventBus();
14+
const handler = vi.fn();
15+
bus.on(GttEvent.MapReady, handler);
16+
const p = payload('ready');
17+
bus.emit(GttEvent.MapReady, p);
18+
expect(handler).toHaveBeenCalledOnce();
19+
expect(handler).toHaveBeenCalledWith(p);
20+
});
21+
22+
it('only delivers to subscribers of the emitted event', () => {
23+
const bus = new GttEventBus();
24+
const other = vi.fn();
25+
bus.on(GttEvent.FeatureSelect, other);
26+
bus.emit(GttEvent.MapReady, payload('ready'));
27+
expect(other).not.toHaveBeenCalled();
28+
});
29+
30+
it('returns an unsubscribe function from on()', () => {
31+
const bus = new GttEventBus();
32+
const handler = vi.fn();
33+
const off = bus.on(GttEvent.GeometryChange, handler);
34+
off();
35+
bus.emit(GttEvent.GeometryChange, payload('geom'));
36+
expect(handler).not.toHaveBeenCalled();
37+
});
38+
39+
it('removes a handler with off()', () => {
40+
const bus = new GttEventBus();
41+
const handler = vi.fn();
42+
bus.on(GttEvent.GeometryChange, handler);
43+
bus.off(GttEvent.GeometryChange, handler);
44+
bus.emit(GttEvent.GeometryChange, payload('geom'));
45+
expect(handler).not.toHaveBeenCalled();
46+
});
47+
48+
it('fires a once() handler only on the first emit', () => {
49+
const bus = new GttEventBus();
50+
const handler = vi.fn();
51+
bus.once(GttEvent.LayersReady, handler);
52+
bus.emit(GttEvent.LayersReady, payload('1'));
53+
bus.emit(GttEvent.LayersReady, payload('2'));
54+
expect(handler).toHaveBeenCalledOnce();
55+
});
56+
57+
it('delivers to every subscriber of an event', () => {
58+
const bus = new GttEventBus();
59+
const a = vi.fn();
60+
const b = vi.fn();
61+
bus.on(GttEvent.MapReady, a);
62+
bus.on(GttEvent.MapReady, b);
63+
bus.emit(GttEvent.MapReady, payload('ready'));
64+
expect(a).toHaveBeenCalledOnce();
65+
expect(b).toHaveBeenCalledOnce();
66+
});
67+
68+
it('isolates a throwing handler: it is logged and the others still run', () => {
69+
const error = vi.spyOn(console, 'error').mockImplementation(() => {});
70+
const bus = new GttEventBus();
71+
const bad = vi.fn(() => { throw new Error('boom'); });
72+
const good = vi.fn();
73+
bus.on(GttEvent.MapReady, bad);
74+
bus.on(GttEvent.MapReady, good);
75+
expect(() => bus.emit(GttEvent.MapReady, payload('ready'))).not.toThrow();
76+
expect(good).toHaveBeenCalledOnce();
77+
expect(error).toHaveBeenCalledOnce();
78+
error.mockRestore();
79+
});
80+
81+
it('does not throw when emitting with no subscribers', () => {
82+
const bus = new GttEventBus();
83+
expect(() => bus.emit(GttEvent.MapReady, payload('ready'))).not.toThrow();
84+
});
85+
});
86+
87+
describe('DOM CustomEvent dispatch', () => {
88+
it('dispatches a bubbling CustomEvent carrying the payload on the DOM target', () => {
89+
const target = new EventTarget();
90+
const bus = new GttEventBus(target as any);
91+
const received: CustomEvent[] = [];
92+
target.addEventListener(GttEvent.GeometryChange, (e) => received.push(e as CustomEvent));
93+
const p = payload('geom');
94+
bus.emit(GttEvent.GeometryChange, p);
95+
expect(received).toHaveLength(1);
96+
expect(received[0].detail).toBe(p);
97+
expect(received[0].bubbles).toBe(true);
98+
});
99+
100+
it('does not dispatch a DOM event when no target is set', () => {
101+
// A null target must simply skip DOM dispatch (no throw); the in-process
102+
// handler still fires.
103+
const bus = new GttEventBus(null);
104+
const handler = vi.fn();
105+
bus.on(GttEvent.MapReady, handler);
106+
expect(() => bus.emit(GttEvent.MapReady, payload('ready'))).not.toThrow();
107+
expect(handler).toHaveBeenCalledOnce();
108+
});
109+
});
110+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { describe, it, expect } from 'vitest';
2+
3+
import {
4+
registerGeocoderProvider,
5+
getGeocoderProvider,
6+
hasGeocoderProvider,
7+
listGeocoderProviders,
8+
} from './registry';
9+
10+
// The registry is a module-level Map shared across this file, so each test
11+
// uses a distinct provider name and list assertions check membership rather
12+
// than exact contents.
13+
const factory = (label: string) => (options: any) => ({
14+
control: { label },
15+
providerOptions: options,
16+
});
17+
18+
describe('geocoder provider registry', () => {
19+
it('registers and retrieves a provider factory', () => {
20+
const f = factory('a');
21+
registerGeocoderProvider('reg-a', f);
22+
expect(getGeocoderProvider('reg-a')).toBe(f);
23+
});
24+
25+
it('reports registration with hasGeocoderProvider', () => {
26+
registerGeocoderProvider('reg-has', factory('h'));
27+
expect(hasGeocoderProvider('reg-has')).toBe(true);
28+
expect(hasGeocoderProvider('reg-absent')).toBe(false);
29+
});
30+
31+
it('returns undefined for an unknown provider', () => {
32+
expect(getGeocoderProvider('reg-unknown')).toBeUndefined();
33+
});
34+
35+
it('overrides a provider when the same name is registered again', () => {
36+
const first = factory('first');
37+
const second = factory('second');
38+
registerGeocoderProvider('reg-override', first);
39+
registerGeocoderProvider('reg-override', second);
40+
expect(getGeocoderProvider('reg-override')).toBe(second);
41+
});
42+
43+
it('lists registered provider names', () => {
44+
registerGeocoderProvider('reg-list-1', factory('1'));
45+
registerGeocoderProvider('reg-list-2', factory('2'));
46+
const names = listGeocoderProviders();
47+
expect(names).toContain('reg-list-1');
48+
expect(names).toContain('reg-list-2');
49+
});
50+
51+
it('passes the provider options through to the factory result', () => {
52+
registerGeocoderProvider('reg-opts', factory('o'));
53+
const result = getGeocoderProvider('reg-opts')!({ reverse: true });
54+
expect(result.providerOptions).toEqual({ reverse: true });
55+
});
56+
});
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
3+
import {
4+
registerLayerFactory,
5+
getLayerFactory,
6+
hasLayerFactory,
7+
listLayerFactories,
8+
getLayerSchema,
9+
listLayerSchemas,
10+
createLayer,
11+
DEFAULT_LAYER_TYPE,
12+
} from './registry';
13+
import type { LayerTypeSchema } from './schema';
14+
15+
// The factories return real OpenLayers layers in production; the registry
16+
// only stores and invokes them, so a sentinel object stands in here.
17+
const fakeLayer = (tag: string) => ({ tag }) as any;
18+
19+
const schemaFor = (type: string): LayerTypeSchema => ({
20+
type,
21+
label: `${type} label`,
22+
options: [{ name: 'url', type: 'url', label: 'URL', required: true }],
23+
});
24+
25+
describe('layer factory registry', () => {
26+
it('defaults to the "ol" layer type', () => {
27+
expect(DEFAULT_LAYER_TYPE).toBe('ol');
28+
});
29+
30+
it('registers, finds, and reports a factory', () => {
31+
const f = vi.fn();
32+
registerLayerFactory('reg-find', f);
33+
expect(getLayerFactory('reg-find')).toBe(f);
34+
expect(hasLayerFactory('reg-find')).toBe(true);
35+
expect(hasLayerFactory('reg-absent')).toBe(false);
36+
expect(listLayerFactories()).toContain('reg-find');
37+
});
38+
39+
it('overrides a factory when re-registered under the same type', () => {
40+
const first = vi.fn();
41+
const second = vi.fn();
42+
registerLayerFactory('reg-override', first);
43+
registerLayerFactory('reg-override', second);
44+
expect(getLayerFactory('reg-override')).toBe(second);
45+
});
46+
47+
describe('schemas', () => {
48+
it('stores a schema keyed by the registration type, overriding the passed type', () => {
49+
// The schema carries a deliberately wrong type to prove the registry
50+
// forces it to the registration name (so the two cannot disagree).
51+
registerLayerFactory('reg-schema', vi.fn(), { ...schemaFor('wrong'), type: 'wrong' });
52+
expect(getLayerSchema('reg-schema')?.type).toBe('reg-schema');
53+
expect(listLayerSchemas().map((s) => s.type)).toContain('reg-schema');
54+
});
55+
56+
it('drops a stale schema when a factory is re-registered without one', () => {
57+
// Regression guard: an override without a schema must not leave the old
58+
// schema behind.
59+
registerLayerFactory('reg-stale', vi.fn(), schemaFor('reg-stale'));
60+
expect(getLayerSchema('reg-stale')).toBeDefined();
61+
registerLayerFactory('reg-stale', vi.fn());
62+
expect(getLayerSchema('reg-stale')).toBeUndefined();
63+
expect(listLayerSchemas().map((s) => s.type)).not.toContain('reg-stale');
64+
});
65+
66+
it('has no schema for a factory registered without one', () => {
67+
registerLayerFactory('reg-noschema', vi.fn());
68+
expect(getLayerSchema('reg-noschema')).toBeUndefined();
69+
});
70+
});
71+
72+
describe('createLayer', () => {
73+
it('falls back to the default type for null/undefined/empty type', () => {
74+
const ol = vi.fn(() => fakeLayer('ol'));
75+
registerLayerFactory(DEFAULT_LAYER_TYPE, ol);
76+
77+
for (const type of [undefined, null, ''] as any[]) {
78+
ol.mockClear();
79+
const layer = createLayer({ name: 'L', type } as any);
80+
expect(ol).toHaveBeenCalledOnce();
81+
expect(layer).toEqual({ tag: 'ol' });
82+
}
83+
});
84+
85+
it('dispatches to the factory matching config.type', () => {
86+
const xyz = vi.fn(() => fakeLayer('xyz'));
87+
registerLayerFactory('reg-xyz', xyz);
88+
const layer = createLayer({ name: 'L', type: 'reg-xyz' } as any);
89+
expect(xyz).toHaveBeenCalledOnce();
90+
expect(layer).toEqual({ tag: 'xyz' });
91+
});
92+
93+
it('returns null and logs for an unknown type, without throwing', () => {
94+
const error = vi.spyOn(console, 'error').mockImplementation(() => {});
95+
const layer = createLayer({ name: 'L', type: 'reg-does-not-exist' } as any);
96+
expect(layer).toBeNull();
97+
expect(error).toHaveBeenCalledOnce();
98+
error.mockRestore();
99+
});
100+
101+
it('returns null and logs when the factory throws, isolating the failure', () => {
102+
const error = vi.spyOn(console, 'error').mockImplementation(() => {});
103+
registerLayerFactory('reg-throws', () => {
104+
throw new Error('bad config');
105+
});
106+
const layer = createLayer({ name: 'L', type: 'reg-throws' } as any);
107+
expect(layer).toBeNull();
108+
expect(error).toHaveBeenCalledOnce();
109+
error.mockRestore();
110+
});
111+
});
112+
});

0 commit comments

Comments
 (0)