maplibre-gl
Version:
BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
544 lines (422 loc) • 24.1 kB
text/typescript
import {AttributionControl, defaultAttributionControlOptions} from './attribution_control';
import {createMap as globalCreateMap, beforeMapTest, sleep} from '../../util/test/util';
import simulate from '../../../test/unit/lib/simulate_interaction';
import {fakeServer} from 'nise';
import {Map} from '../../ui/map';
import {MapSourceDataEvent} from '../events';
function createMap() {
return globalCreateMap({
attributionControl: false,
style: {
version: 8,
sources: {},
layers: [],
owner: 'maplibre',
id: 'demotiles',
},
hash: true
}, undefined);
}
let map: Map;
beforeEach(() => {
beforeMapTest();
map = createMap();
});
afterEach(() => {
map.remove();
});
describe('AttributionControl', () => {
test('appears in bottom-right by default', () => {
map.addControl(new AttributionControl());
expect(
map.getContainer().querySelectorAll('.maplibregl-ctrl-bottom-right .maplibregl-ctrl-attrib')
).toHaveLength(1);
});
test('appears in the position specified by the position option', () => {
map.addControl(new AttributionControl(), 'top-left');
expect(
map.getContainer().querySelectorAll('.maplibregl-ctrl-top-left .maplibregl-ctrl-attrib')
).toHaveLength(1);
});
test('appears in compact mode if compact option is used', () => {
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 700, configurable: true});
let attributionControl = new AttributionControl({
compact: true,
customAttribution: 'MapLibre'
});
map.addControl(attributionControl);
const container = map.getContainer();
expect(
container.querySelectorAll('.maplibregl-ctrl-attrib.maplibregl-compact')
).toHaveLength(1);
map.removeControl(attributionControl);
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 600, configurable: true});
attributionControl = new AttributionControl({
compact: false
});
map.addControl(attributionControl);
expect(
container.querySelectorAll('.maplibregl-ctrl-attrib:not(.maplibregl-compact)')
).toHaveLength(1);
});
test('appears in compact mode if container is less then 640 pixel wide and attributions are not empty', () => {
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 700, configurable: true});
const attributionControl = new AttributionControl({
customAttribution: 'MapLibre'
});
map.addControl(attributionControl);
const container = map.getContainer();
expect(
container.querySelectorAll('.maplibregl-ctrl-attrib:not(.maplibregl-compact)')
).toHaveLength(1);
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 600, configurable: true});
map.resize();
expect(
container.querySelectorAll('.maplibregl-ctrl-attrib.maplibregl-compact')
).toHaveLength(1);
expect(
container.querySelectorAll('.maplibregl-attrib-empty')
).toHaveLength(0);
});
test('does not appear in compact mode if container is less then 640 pixel wide and attributions are empty', () => {
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 700, configurable: true});
map.addControl(new AttributionControl({}));
const container = map.getContainer();
expect(
container.querySelectorAll('.maplibregl-ctrl-attrib:not(.maplibregl-compact)')
).toHaveLength(1);
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 600, configurable: true});
map.resize();
expect(
container.querySelectorAll('.maplibregl-ctrl-attrib.maplibregl-compact')
).toHaveLength(0);
expect(
container.querySelectorAll('.maplibregl-attrib-empty')
).toHaveLength(1);
});
test('compact mode control toggles attribution', () => {
map.addControl(new AttributionControl({
compact: true,
customAttribution: 'MapLibre'
}));
const container = map.getContainer();
const toggle = container.querySelector('.maplibregl-ctrl-attrib-button');
expect(container.querySelectorAll('.maplibregl-compact-show')).toHaveLength(1);
simulate.click(toggle);
expect(container.querySelectorAll('.maplibregl-compact-show')).toHaveLength(0);
simulate.click(toggle);
expect(container.querySelectorAll('.maplibregl-compact-show')).toHaveLength(1);
});
test('dedupes attributions that are substrings of others', async () => {
const attribution = new AttributionControl();
map.addControl(attribution);
const spy = jest.fn();
map.on('data', spy);
await map.once('load');
map.addSource('1', {type: 'geojson', data: {type: 'FeatureCollection', features: []}, attribution: 'World'});
map.addSource('2', {type: 'geojson', data: {type: 'FeatureCollection', features: []}, attribution: 'Hello World'});
map.addSource('3', {type: 'geojson', data: {type: 'FeatureCollection', features: []}, attribution: 'Another Source'});
map.addSource('4', {type: 'geojson', data: {type: 'FeatureCollection', features: []}, attribution: 'Hello'});
map.addSource('5', {type: 'geojson', data: {type: 'FeatureCollection', features: []}, attribution: 'Hello World'});
map.addSource('6', {type: 'geojson', data: {type: 'FeatureCollection', features: []}, attribution: 'Hello World'});
map.addSource('7', {type: 'geojson', data: {type: 'FeatureCollection', features: []}, attribution: 'GeoJSON Source'});
map.addLayer({id: '1', type: 'fill', source: '1'});
map.addLayer({id: '2', type: 'fill', source: '2'});
map.addLayer({id: '3', type: 'fill', source: '3'});
map.addLayer({id: '4', type: 'fill', source: '4'});
map.addLayer({id: '5', type: 'fill', source: '5'});
map.addLayer({id: '6', type: 'fill', source: '6'});
map.addLayer({id: '7', type: 'fill', source: '7'});
await sleep(100);
expect(attribution._innerContainer.innerHTML).toBe(`Hello World | Another Source | GeoJSON Source | ${defaultAttributionControlOptions.customAttribution}`);
expect(spy.mock.calls.filter((call) => call[0].dataType === 'source' && call[0].sourceDataType === 'visibility')).toHaveLength(7);
});
test('is hidden if empty', async () => {
const attribution = new AttributionControl({});
map.addControl(attribution);
await map.once('load');
map.addSource('1', {type: 'geojson', data: {type: 'FeatureCollection', features: []}});
map.addLayer({id: '1', type: 'fill', source: '1'});
const container = map.getContainer();
const spy = jest.fn();
map.on('data', spy);
await sleep(100);
expect(spy.mock.calls.filter((call) => call[0].dataType === 'source' && call[0].sourceDataType === 'visibility')).toHaveLength(1);
expect(attribution._innerContainer.innerHTML).toBe('');
expect(container.querySelectorAll('.maplibregl-attrib-empty')).toHaveLength(1);
});
test('is not hidden if adding a source with attribution', async () => {
const attribution = new AttributionControl({});
map.addControl(attribution);
await map.once('load');
map.addSource('1', {type: 'geojson', data: {type: 'FeatureCollection', features: []}});
map.addLayer({id: '1', type: 'fill', source: '1'});
const container = map.getContainer();
const spy = jest.fn();
map.on('data', spy);
await sleep(100);
map.addSource('2', {type: 'geojson', data: {type: 'FeatureCollection', features: []}, attribution: 'Hello World'});
map.addLayer({id: '2', type: 'fill', source: '2'});
await sleep(100);
expect(spy.mock.calls.filter((call) => call[0].dataType === 'source' && call[0].sourceDataType === 'visibility')).toHaveLength(2);
expect(attribution._innerContainer.innerHTML).toBe('Hello World');
expect(container.querySelectorAll('.maplibregl-attrib-empty')).toHaveLength(0);
});
test('shows custom attribution if customAttribution option is provided', () => {
const attributionControl = new AttributionControl({
customAttribution: 'Custom string'
});
map.addControl(attributionControl);
expect(attributionControl._innerContainer.innerHTML).toBe('Custom string');
});
test('shows custom attribution if customAttribution option is provided, control is removed and added back', () => {
const attributionControl = new AttributionControl({
customAttribution: 'Custom string'
});
map.addControl(attributionControl);
map.removeControl(attributionControl);
map.addControl(attributionControl);
expect(attributionControl._innerContainer.innerHTML).toBe('Custom string');
});
test('in compact mode shows custom attribution if customAttribution option is provided', () => {
const attributionControl = new AttributionControl({
customAttribution: 'Custom string',
compact: true
});
map.addControl(attributionControl);
expect(attributionControl._innerContainer.innerHTML).toBe('Custom string');
});
test('shows all custom attributions if customAttribution array of strings is provided', () => {
const attributionControl = new AttributionControl({
customAttribution: ['Some very long custom string', 'Custom string', 'Another custom string']
});
map.addControl(attributionControl);
expect(attributionControl._innerContainer.innerHTML).toBe('Custom string | Another custom string | Some very long custom string');
});
test('hides attributions for sources that are not currently visible', async () => {
const attribution = new AttributionControl();
map.addControl(attribution);
const spy = jest.fn();
map.on('data', spy);
await map.once('load');
map.addSource('1', {type: 'geojson', data: {type: 'FeatureCollection', features: []}, attribution: 'Used'});
map.addSource('2', {type: 'geojson', data: {type: 'FeatureCollection', features: []}, attribution: 'Not used'});
map.addSource('3', {type: 'geojson', data: {type: 'FeatureCollection', features: []}, attribution: 'Visibility none'});
map.addLayer({id: 'layer1', type: 'fill', source: '1'});
map.addLayer({id: 'layer3', type: 'fill', source: '3', layout: {visibility: 'none'}});
await sleep(100);
expect(spy.mock.calls.filter((call) => {
const mapDataEvent: MapSourceDataEvent = call[0];
// the only one visible should be '1'.
// source 2 does not have layer and source 3 is not visible
return mapDataEvent.dataType === 'source' &&
mapDataEvent.sourceDataType === 'visibility' &&
mapDataEvent.sourceId === '1';
})).toHaveLength(1);
expect(attribution._innerContainer.innerHTML).toBe(`Used | ${defaultAttributionControlOptions.customAttribution}`);
});
test('does not show attributions for sources that are used for terrain when they are not in use', async () => {
global.fetch = null;
const server = fakeServer.create();
server.respondWith('/source.json', JSON.stringify({
minzoom: 5,
maxzoom: 12,
attribution: 'Terrain',
tiles: ['http://example.com/{z}/{x}/{y}.pngraw'],
bounds: [-47, -7, -45, -5]
}));
const attribution = new AttributionControl();
map.addControl(attribution);
const spy = jest.fn();
map.on('data', spy);
await map.once('load');
map.addSource('1', {type: 'raster-dem', url: '/source.json'});
server.respond();
await sleep(100);
// there should not be a visibility event since there is no layer
expect(spy.mock.calls.filter((call) => {
const mapDataEvent: MapSourceDataEvent = call[0];
return mapDataEvent.dataType === 'source' &&
mapDataEvent.sourceDataType === 'visibility';
})).toHaveLength(0);
expect(attribution._innerContainer.innerHTML).toBe(defaultAttributionControlOptions.customAttribution);
});
test('shows attributions for sources that are used for terrain', async () => {
global.fetch = null;
const server = fakeServer.create();
server.respondWith('/source.json', JSON.stringify({
minzoom: 5,
maxzoom: 12,
attribution: 'Terrain',
tiles: ['http://example.com/{z}/{x}/{y}.pngraw'],
bounds: [-47, -7, -45, -5]
}));
const attribution = new AttributionControl();
map.addControl(attribution);
const spy = jest.fn();
map.on('data', spy);
await map.once('load');
map.addSource('1', {type: 'raster-dem', url: '/source.json'});
server.respond();
map.setTerrain({source: '1'});
await sleep(100);
// there should not be a visibility event since there is no layer
expect(spy.mock.calls.filter((call) => {
const mapDataEvent: MapSourceDataEvent = call[0];
return mapDataEvent.dataType === 'source' &&
mapDataEvent.sourceDataType === 'visibility';
})).toHaveLength(0);
expect(attribution._innerContainer.innerHTML).toBe(`Terrain | ${defaultAttributionControlOptions.customAttribution}`);
});
test('toggles attributions for sources whose visibility changes when zooming', async () => {
const attribution = new AttributionControl({});
map.addControl(attribution);
map.on('data', (e) => {
if (e.dataType === 'source' && e.sourceDataType === 'metadata') {
map.setZoom(13);
}
});
await map.once('load');
map.addSource('1', {type: 'geojson', data: {type: 'FeatureCollection', features: []}, attribution: 'Used'});
map.addLayer({id: '1', type: 'fill', source: '1', minzoom: 12});
await sleep(100);
expect(map.getZoom()).toBe(13);
expect(attribution._innerContainer.innerHTML).toBe('Used');
});
});
describe('AttributionControl test regarding the HTML elements details and summary', () => {
describe('Details is set correct for compact view', () => {
test('It should NOT contain the attribute open="" on first load.', () => {
const attributionControl = new AttributionControl({
compact: true,
});
map.addControl(attributionControl);
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBeNull();
});
test('It SHOULD contain the attribute open="" after click on summary.', () => {
const attributionControl = new AttributionControl({
compact: true,
});
map.addControl(attributionControl);
const container = map.getContainer();
const toggle = container.querySelector('.maplibregl-ctrl-attrib-button');
simulate.click(toggle);
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBe('');
});
test('It should NOT contain the attribute open="" after two clicks on summary.', () => {
const attributionControl = new AttributionControl({
compact: true,
});
map.addControl(attributionControl);
const container = map.getContainer();
const toggle = container.querySelector('.maplibregl-ctrl-attrib-button');
simulate.click(toggle);
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBe('');
simulate.click(toggle);
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBeNull();
});
});
describe('Details is set correct for default view (compact === undefined)', () => {
test('It should NOT contain the attribute open="" if offsetWidth <= 640.', () => {
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 640, configurable: true});
const attributionControl = new AttributionControl({
customAttribution: undefined
});
map.addControl(attributionControl);
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBeNull();
});
test('It SHOULD contain the attribute open="" if offsetWidth > 640.', () => {
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 641, configurable: true});
const attributionControl = new AttributionControl({});
map.addControl(attributionControl);
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBe('');
});
test('The attribute open="" SHOULD exist after resize from size > 640 to <= 640 and and vice versa.', () => {
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 640, configurable: true});
const attributionControl = new AttributionControl({
customAttribution: 'MapLibre'
});
map.addControl(attributionControl);
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib.maplibregl-compact')).toHaveLength(1);
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBe('');
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 641, configurable: true});
map.resize();
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib:not(.maplibregl-compact)')).toHaveLength(1);
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBe('');
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 640, configurable: true});
map.resize();
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib.maplibregl-compact')).toHaveLength(1);
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBe('');
});
test('The attribute open="" should NOT change on resize from > 640 to another > 640.', () => {
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 641, configurable: true});
const attributionControl = new AttributionControl({});
map.addControl(attributionControl);
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBe('');
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 650, configurable: true});
map.resize();
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBe('');
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 641, configurable: true});
map.resize();
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBe('');
});
test('The attribute open="" should NOT change on resize from <= 640 to another <= 640 if it is closed.', () => {
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 640, configurable: true});
const attributionControl = new AttributionControl({});
map.addControl(attributionControl);
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBeNull();
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 630, configurable: true});
map.resize();
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBeNull();
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 640, configurable: true});
map.resize();
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBeNull();
});
test('The attribute open="" should NOT change on resize from <= 640 to another <= 640 if it is open.', () => {
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 640, configurable: true});
const attributionControl = new AttributionControl({});
map.addControl(attributionControl);
const toggle = map.getContainer().querySelector('.maplibregl-ctrl-attrib-button');
simulate.click(toggle);
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBe('');
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 630, configurable: true});
map.resize();
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBe('');
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 640, configurable: true});
map.resize();
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBe('');
});
});
describe('Details is set correct for default view (compact === false)', () => {
test('It SHOULD contain the attribute open="" if offsetWidth <= 640.', () => {
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 640, configurable: true});
const attributionControl = new AttributionControl({
compact: false
});
map.addControl(attributionControl);
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBe('');
});
test('It SHOULD contain the attribute open="" if offsetWidth > 640.', () => {
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 641, configurable: true});
const attributionControl = new AttributionControl({
compact: false
});
map.addControl(attributionControl);
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBe('');
});
test('The attribute open="" should NOT change on resize.', () => {
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 640, configurable: true});
const attributionControl = new AttributionControl({
compact: false
});
map.addControl(attributionControl);
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBe('');
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 641, configurable: true});
map.resize();
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBe('');
Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 640, configurable: true});
map.resize();
expect(map.getContainer().querySelectorAll('.maplibregl-ctrl-attrib')[0].getAttribute('open')).toBe('');
});
});
});