@maplibre/maplibre-gl-style-spec
Version:
a specification for maplibre styles
200 lines (174 loc) • 7.49 kB
text/typescript
import {
createPropertyExpression,
Feature,
GlobalProperties,
StylePropertyExpression
} from '../expression';
import {expressions} from './definitions';
import v8 from '../reference/v8.json' with {type: 'json'};
import {createExpression, StylePropertySpecification} from '..';
import {ExpressionParsingError} from './parsing_error';
import {VariableAnchorOffsetCollection} from './types/variable_anchor_offset_collection';
import {describe, test, expect, vi} from 'vitest';
// filter out internal "error" and "filter-*" expressions from definition list
const filterExpressionRegex = /filter-/;
const definitionList = Object.keys(expressions)
.filter((expression) => {
return expression !== 'error' && !filterExpressionRegex.exec(expression);
})
.sort();
test('v8.json includes all definitions from style-spec', () => {
const v8List = Object.keys(v8.expression_name.values);
const v8SupportedList = v8List.filter((expression) => {
//filter out expressions that are not supported in GL-JS
return !!v8.expression_name.values[expression]['sdk-support']['basic functionality']['js'];
});
expect(definitionList).toEqual(v8SupportedList.sort());
});
describe('createPropertyExpression', () => {
test('prohibits non-interpolable properties from using an "interpolate" expression', () => {
const {result, value} = createPropertyExpression(
['interpolate', ['linear'], ['zoom'], 0, 0, 10, 10],
{
type: 'number',
'property-type': 'data-constant',
expression: {
interpolated: false,
parameters: ['zoom']
}
} as StylePropertySpecification
);
expect(result).toBe('error');
expect(value as ExpressionParsingError[]).toHaveLength(1);
expect(value[0].message).toBe(
'"interpolate" expressions cannot be used with this property'
);
});
test('sets globalStateRefs', () => {
const {value} = createPropertyExpression(
[
'case',
['>', ['global-state', 'stateKey'], 0],
100,
['global-state', 'anotherStateKey']
],
{
type: 'number',
'property-type': 'data-driven',
expression: {
interpolated: false,
parameters: ['zoom', 'feature']
}
} as any as StylePropertySpecification
) as {value: StylePropertyExpression};
expect(value.globalStateRefs).toEqual(new Set(['stateKey', 'anotherStateKey']));
});
});
describe('evaluate expression', () => {
test('silently falls back to default for nullish values', () => {
const {value} = createPropertyExpression(['global-state', 'x'], {
type: null,
default: 42,
'property-type': 'data-driven',
transition: false
}) as {value: StylePropertyExpression};
vi.spyOn(console, 'warn').mockImplementation(() => {});
expect(value.evaluate({globalState: {x: 5}, zoom: 10} as GlobalProperties)).toBe(5);
expect(console.warn).not.toHaveBeenCalled();
expect(value.evaluate({globalState: {}, zoom: 10} as GlobalProperties)).toBe(42);
expect(console.warn).not.toHaveBeenCalled();
});
test('global state as expression property', () => {
const {value} = createPropertyExpression(
['global-state', 'x'],
{
type: null,
default: 42,
'property-type': 'data-driven',
transition: false
},
{x: 5}
) as {value: StylePropertyExpression};
vi.spyOn(console, 'warn').mockImplementation(() => {});
expect(value.evaluate({globalState: {x: 15}, zoom: 10} as GlobalProperties)).toBe(5);
expect(console.warn).not.toHaveBeenCalled();
});
test('global state as expression property of zoom dependent expression', () => {
const {value} = createPropertyExpression(
['interpolate', ['linear'], ['zoom'], 10, ['global-state', 'x'], 20, 50],
{
type: 'number',
default: 42,
'property-type': 'data-driven',
expression: {
interpolated: true,
parameters: ['zoom']
}
} as StylePropertySpecification,
{x: 5}
) as {value: StylePropertyExpression};
vi.spyOn(console, 'warn').mockImplementation(() => {});
expect(value.evaluate({globalState: {x: 15}, zoom: 10} as GlobalProperties)).toBe(5);
expect(console.warn).not.toHaveBeenCalled();
});
test('warns and falls back to default for invalid enum values', () => {
const {value} = createPropertyExpression(['get', 'x'], {
type: 'enum',
values: {a: {}, b: {}, c: {}},
default: 'a',
'property-type': 'data-driven',
expression: {
interpolated: false,
parameters: ['zoom', 'feature']
}
} as any as StylePropertySpecification) as {value: StylePropertyExpression};
vi.spyOn(console, 'warn').mockImplementation(() => {});
expect(value.kind).toBe('source');
expect(
value.evaluate({} as GlobalProperties, {properties: {x: 'b'}} as any as Feature)
).toBe('b');
expect(
value.evaluate({} as GlobalProperties, {properties: {x: 'invalid'}} as any as Feature)
).toBe('a');
expect(console.warn).toHaveBeenCalledWith(
'Expected value to be one of "a", "b", "c", but found "invalid" instead.'
);
});
test('warns for invalid variableAnchorOffsetCollection values', () => {
const {value} = createPropertyExpression(['get', 'x'], {
type: 'variableAnchorOffsetCollection',
'property-type': 'data-driven',
transition: false,
expression: {
interpolated: false,
parameters: ['zoom', 'feature']
}
}) as {value: StylePropertyExpression};
const warnMock = vi.spyOn(console, 'warn').mockImplementation(() => {});
expect(value.kind).toBe('source');
expect(
value.evaluate({} as GlobalProperties, {properties: {x: 'invalid'}} as any as Feature)
).toBeNull();
expect(console.warn).toHaveBeenCalledTimes(1);
expect(console.warn).toHaveBeenCalledWith(
"Could not parse variableAnchorOffsetCollection from value 'invalid'"
);
warnMock.mockClear();
expect(
value.evaluate(
{} as GlobalProperties,
{properties: {x: ['top', [2, 2]]}} as any as Feature
)
).toEqual(new VariableAnchorOffsetCollection(['top', [2, 2]]));
expect(console.warn).not.toHaveBeenCalled();
});
});
describe('nonexistent operators', () => {
test('"ExpressionSpecification" operator does not exist', () => {
const response = createExpression(['ExpressionSpecification']);
expect(response.result).toBe('error');
expect((response.value as ExpressionParsingError[])[0].message).toContain(
'Unknown expression \"ExpressionSpecification\".'
);
});
});