maplibre-gl
Version:
BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
709 lines (517 loc) • 22.4 kB
text/typescript
import {describe, beforeEach, afterEach, test, expect} from 'vitest';
import {Hash} from './hash';
import {createMap as globalCreateMap, beforeMapTest} from '../util/test/util';
import type {Map} from './map';
describe('hash', () => {
function createHash(name: string = undefined) {
const hash = new Hash(name);
hash._updateHash = hash._updateHashUnthrottled.bind(hash);
return hash;
}
function createMap() {
const container = window.document.createElement('div');
Object.defineProperty(container, 'clientWidth', {value: 512});
Object.defineProperty(container, 'clientHeight', {value: 512});
return globalCreateMap({container});
}
let map: Map;
beforeEach(() => {
beforeMapTest();
map = createMap();
});
afterEach(() => {
if (map._removed === false) {
map.remove();
}
window.location.hash = '';
});
test('addTo', () => {
const hash = createHash();
expect(hash._map).toBeFalsy();
hash.addTo(map);
expect(hash._map).toBeTruthy();
});
test('remove', () => {
const hash = createHash()
.addTo(map);
expect(hash._map).toBeTruthy();
hash.remove();
expect(hash._map).toBeFalsy();
});
test('_onHashChange', () => {
const hash = createHash()
.addTo(map);
window.location.hash = '#10/3.00/-1.00';
hash._onHashChange();
expect(map.getCenter().lng).toBe(-1);
expect(map.getCenter().lat).toBe(3);
expect(map.getZoom()).toBe(10);
expect(map.getBearing()).toBe(0);
expect(map.getPitch()).toBe(0);
// map is created with `interactive: false`
// so explicitly enable rotation for this test
map.dragRotate.enable();
map.touchZoomRotate.enable();
window.location.hash = '#5/1.00/0.50/30/60';
hash._onHashChange();
expect(map.getCenter().lng).toBe(0.5);
expect(map.getCenter().lat).toBe(1);
expect(map.getZoom()).toBe(5);
expect(map.getBearing()).toBe(30);
expect(map.getPitch()).toBe(60);
// disable rotation to test that updating
// the hash's bearing won't change the map
map.dragRotate.disable();
map.touchZoomRotate.disable();
window.location.hash = '#5/1.00/0.50/-45/60';
hash._onHashChange();
expect(map.getCenter().lng).toBe(0.5);
expect(map.getCenter().lat).toBe(1);
expect(map.getZoom()).toBe(5);
expect(map.getBearing()).toBe(30);
expect(map.getPitch()).toBe(60);
// test that a hash with no bearing resets
// to the previous bearing when rotation is disabled
window.location.hash = '#5/1.00/0.50/';
hash._onHashChange();
expect(map.getCenter().lng).toBe(0.5);
expect(map.getCenter().lat).toBe(1);
expect(map.getZoom()).toBe(5);
expect(map.getBearing()).toBe(30);
expect(window.location.hash).toBe('#5/1/0.5/30');
window.location.hash = '#4/wrongly/formed/hash';
expect(hash._onHashChange()).toBeFalsy();
});
test('_onHashChange empty', () => {
const hash = createHash()
.addTo(map);
window.location.hash = '#10/3.00/-1.00';
hash._onHashChange();
expect(map.getCenter().lng).toBe(-1);
expect(map.getCenter().lat).toBe(3);
expect(map.getZoom()).toBe(10);
expect(map.getBearing()).toBe(0);
expect(map.getPitch()).toBe(0);
window.location.hash = '';
hash._onHashChange();
expect(map.getCenter().lng).toBe(-1);
expect(map.getCenter().lat).toBe(3);
expect(map.getZoom()).toBe(10);
expect(map.getBearing()).toBe(0);
expect(map.getPitch()).toBe(0);
});
test('_onHashChange named', () => {
const hash = createHash('map')
.addTo(map);
window.location.hash = '#map=10/3.00/-1.00&foo=bar';
hash._onHashChange();
expect(map.getCenter().lng).toBe(-1);
expect(map.getCenter().lat).toBe(3);
expect(map.getZoom()).toBe(10);
expect(map.getBearing()).toBe(0);
expect(map.getPitch()).toBe(0);
window.location.hash = '#map&foo=bar';
expect(hash._onHashChange()).toBeFalsy();
window.location.hash = '#map=4/5/baz&foo=bar';
expect(hash._onHashChange()).toBeFalsy();
window.location.hash = '#5/1.00/0.50/30/60';
expect(hash._onHashChange()).toBeFalsy();
});
test('_getCurrentHash', () => {
const hash = createHash()
.addTo(map);
window.location.hash = '#10/3.00/-1.00';
expect(hash._getCurrentHash()).toStrictEqual(['10', '3.00', '-1.00']);
});
test('_getCurrentHash named', () => {
const hash = createHash('map')
.addTo(map);
window.location.hash = '#map=10/3.00/-1.00&foo=bar';
expect(hash._getCurrentHash()).toStrictEqual(['10', '3.00', '-1.00']);
window.location.hash = '#baz&map=10/3.00/-1.00';
expect(hash._getCurrentHash()).toStrictEqual(['10', '3.00', '-1.00']);
});
describe('getHashString', () => {
let hash: Hash;
beforeEach(() => {
hash = createHash()
.addTo(map);
});
test('mapFeedback=true', () => {
map.setZoom(10);
map.setCenter([2.5, 3.75]);
const hashStringWithFeedback = hash.getHashString(true);
expect(hashStringWithFeedback).toBe('#/2.5/3.75/10');
map.setBearing(45);
map.setPitch(30);
const hashStringWithRotationAndFeedback = hash.getHashString(true);
expect(hashStringWithRotationAndFeedback).toBe('#/2.5/3.75/10/45/30');
});
test('mapFeedback=false', () => {
map.setZoom(10);
map.setCenter([2.5, 3.75]);
const hashStringWithoutFeedback = hash.getHashString(false);
expect(hashStringWithoutFeedback).toBe('#10/3.75/2.5');
map.setBearing(45);
map.setPitch(30);
const hashStringWithRotationAndWithoutFeedback = hash.getHashString(false);
expect(hashStringWithRotationAndWithoutFeedback).toBe('#10/3.75/2.5/45/30');
});
});
test('_updateHash', () => {
createHash()
.addTo(map);
expect(window.location.hash).toBeFalsy();
map.setZoom(3);
map.setCenter([2.0, 1.0]);
expect(window.location.hash).toBe('#3/1/2');
map.setPitch(60);
expect(window.location.hash).toBe('#3/1/2/0/60');
map.setBearing(135);
expect(window.location.hash).toBe('#3/1/2/135/60');
});
test('_updateHash named', () => {
createHash('map')
.addTo(map);
expect(window.location.hash).toBeFalsy();
window.location.hash = '';
map.setZoom(3);
map.setCenter([1.0, 2.0]);
expect(window.location.hash).toBeTruthy();
expect(window.location.hash).toBe('#map=3/2/1');
map.setPitch(60);
expect(window.location.hash).toBe('#map=3/2/1/0/60');
map.setBearing(135);
expect(window.location.hash).toBe('#map=3/2/1/135/60');
window.location.hash += '&foo=bar';
map.setZoom(7);
expect(window.location.hash).toBe('#map=7/2/1/135/60&foo=bar');
window.location.hash = '#baz&map=7/2/1/135/60&foo=bar';
map.setCenter([2.0, 1.0]);
expect(window.location.hash).toBe('#baz&map=7/1/2/135/60&foo=bar');
});
describe('_removeHash without a name', () => {
let hash: Hash;
beforeEach(() => {
hash = createHash()
.addTo(map);
});
test('removes hash when hash is only map hash', () => {
map.setZoom(3);
map.setCenter([2.0, 1.0]);
expect(window.location.hash).toBe('#3/1/2');
hash._removeHash();
expect(window.location.hash).toBe('');
});
test('removes hash when hash contains other parameters', () => {
window.location.hash = '#3/1/2&foo=bar';
hash._removeHash();
expect(window.location.hash).toBe('#foo=bar');
});
});
describe('_removeHash with a name', () => {
let hash: Hash;
beforeEach(() => {
hash = createHash('map')
.addTo(map);
});
test('removes hash when hash is only map hash', () => {
map.setZoom(3);
map.setCenter([2.0, 1.0]);
expect(window.location.hash).toBe('#map=3/1/2');
hash._removeHash();
expect(window.location.hash).toBe('');
});
test('removes hash when hash contains other parameters at end', () => {
window.location.hash = '#map=3/1/2&foo=bar';
hash._removeHash();
expect(window.location.hash).toBe('#foo=bar');
});
test('removes hash when hash contains other parameters at start and end', () => {
window.location.hash = '#baz&map=7/2/1/135/60&foo=bar';
hash._removeHash();
expect(window.location.hash).toBe('#baz&foo=bar');
});
});
describe('_isValidHash', () => {
let hash: Hash;
beforeEach(() => {
hash = createHash()
.addTo(map);
});
test('validate hash with zoom and center only', () => {
window.location.hash = '#10/3.00/-1.00';
expect(hash._isValidHash(hash._getCurrentHash())).toBeTruthy();
});
test('validate hash with bearing and pitch', () => {
window.location.hash = '#5/1.00/0.50/30/60';
expect(hash._isValidHash(hash._getCurrentHash())).toBeTruthy();
});
test('validate hash with negative bearing and positive pitch', () => {
window.location.hash = '#5/1.00/0.50/-30/60';
expect(hash._isValidHash(hash._getCurrentHash())).toBeTruthy();
});
test('validate hash with positive bearing and negative pitch', () => {
window.location.hash = '#5/1.00/0.50/-30/60';
expect(hash._isValidHash(hash._getCurrentHash())).toBeTruthy();
});
test('validate hash with bearing only', () => {
window.location.hash = '#5/1.00/0.50/30';
expect(hash._isValidHash(hash._getCurrentHash())).toBeTruthy();
});
test('validate hash with negative bearing only', () => {
window.location.hash = '#5/1.00/0.50/30';
expect(hash._isValidHash(hash._getCurrentHash())).toBeTruthy();
});
test('invalidate hash with slashes encoded as %2F', () => {
window.location.hash = '#10%2F3.00%2F-1.00';
expect(hash._isValidHash(hash._getCurrentHash())).toBeFalsy();
});
test('invalidate hash with string values', () => {
window.location.hash = '#4/wrongly/formed/hash';
expect(hash._isValidHash(hash._getCurrentHash())).toBeFalsy();
});
test('invalidate hash that is named, but should not be', () => {
window.location.hash = '#map=10/3.00/-1.00&foo=bar';
expect(hash._isValidHash(hash._getCurrentHash())).toBeFalsy();
});
test('invalidate hash that has the coord as the second value, but should be first or use named params', () => {
window.location.hash = '#foo=bar&10/3.00/-1.00';
expect(hash._isValidHash(hash._getCurrentHash())).toBeFalsy();
});
test('invalidate hash, only one value', () => {
window.location.hash = '#24';
expect(hash._isValidHash(hash._getCurrentHash())).toBeFalsy();
});
test('invalidate hash, only two values', () => {
window.location.hash = '#24/3.00';
expect(hash._isValidHash(hash._getCurrentHash())).toBeFalsy();
});
test('invalidate hash, zoom greater than maxZoom', () => {
window.location.hash = '#24/3.00/-1.00';
expect(hash._isValidHash(hash._getCurrentHash())).toBeFalsy();
});
test('invalidate hash, latitude out of range', () => {
window.location.hash = '#10/100.00/-1.00';
expect(hash._isValidHash(hash._getCurrentHash())).toBeFalsy();
});
test('invalidate hash, bearing out of range', () => {
window.location.hash = '#10/3.00/-1.00/450';
expect(hash._isValidHash(hash._getCurrentHash())).toBeFalsy();
window.location.hash = '#10/3.00/-1.00/-450';
expect(hash._isValidHash(hash._getCurrentHash())).toBeFalsy();
});
test('invalidate hash, pitch greater than maxPitch', () => {
window.location.hash = '#10/3.00/-1.00/30/90';
expect(hash._isValidHash(hash._getCurrentHash())).toBeFalsy();
});
});
describe('initialization', () => {
test('http://localhost/#', () => {
window.location.href = 'http://localhost/#';
createHash().addTo(map);
map.setZoom(3);
expect(window.location.hash).toBe('#3/0/0');
expect(window.location.href).toBe('http://localhost/#3/0/0');
map.setCenter([2.0, 1.0]);
expect(window.location.hash).toBe('#3/1/2');
expect(window.location.href).toBe('http://localhost/#3/1/2');
});
test('http://localhost/##', () => {
window.location.href = 'http://localhost/##';
createHash().addTo(map);
map.setZoom(3);
expect(window.location.hash).toBe('#3/0/0');
expect(window.location.href).toBe('http://localhost/#3/0/0');
map.setCenter([2.0, 1.0]);
expect(window.location.hash).toBe('#3/1/2');
expect(window.location.href).toBe('http://localhost/#3/1/2');
});
test('http://localhost#', () => {
window.location.href = 'http://localhost#';
createHash().addTo(map);
map.setZoom(4);
expect(window.location.hash).toBe('#4/0/0');
expect(window.location.href).toBe('http://localhost/#4/0/0');
map.setCenter([2.0, 1.0]);
expect(window.location.hash).toBe('#4/1/2');
expect(window.location.href).toBe('http://localhost/#4/1/2');
});
test('http://localhost/', () => {
window.location.href = 'http://localhost/';
createHash().addTo(map);
map.setZoom(5);
expect(window.location.hash).toBe('#5/0/0');
expect(window.location.href).toBe('http://localhost/#5/0/0');
map.setCenter([2.0, 1.0]);
expect(window.location.hash).toBe('#5/1/2');
expect(window.location.href).toBe('http://localhost/#5/1/2');
});
test('default value for window.location.href', () => {
createHash().addTo(map);
map.setZoom(5);
expect(window.location.hash).toBe('#5/0/0');
expect(window.location.href).toBe('http://localhost/#5/0/0');
map.setCenter([2.0, 1.0]);
expect(window.location.hash).toBe('#5/1/2');
expect(window.location.href).toBe('http://localhost/#5/1/2');
});
test('http://localhost', () => {
window.location.href = 'http://localhost';
createHash().addTo(map);
map.setZoom(4);
expect(window.location.hash).toBe('#4/0/0');
expect(window.location.href).toBe('http://localhost/#4/0/0');
map.setCenter([2.0, 1.0]);
expect(window.location.hash).toBe('#4/1/2');
expect(window.location.href).toBe('http://localhost/#4/1/2');
});
});
test('map.remove', () => {
const container = window.document.createElement('div');
Object.defineProperty(container, 'clientWidth', {value: 512});
Object.defineProperty(container, 'clientHeight', {value: 512});
map.remove();
});
test('hash with URL in other parameter does not change', () => {
const hash = createHash('map')
.addTo(map);
// Set up hash with URL in another parameter
window.location.hash = '#map=10/3/-1&returnUrl=https://example.com&filter=a&b=';
map.setZoom(5);
map.setCenter([1.0, 2.0]);
expect(window.location.hash).toBe('#map=5/2/1&returnUrl=https://example.com&filter=a&b=');
window.location.hash = '#search=foo&map=7/4/2&redirect=/path?query=value';
hash._onHashChange();
expect(map.getZoom()).toBe(7);
expect(map.getCenter().lat).toBe(4);
expect(map.getCenter().lng).toBe(2);
});
test('hash with URL+path in other parameter does not change', () => {
const hash = createHash('map')
.addTo(map);
// Set up hash with URL in another parameter
window.location.hash = '#map=10/3/-1&returnUrl=https://example.com/abcd/ef&filter=a&b=';
map.setZoom(5);
map.setCenter([1.0, 2.0]);
expect(window.location.hash).toBe('#map=5/2/1&returnUrl=https://example.com/abcd/ef&filter=a&b=');
window.location.hash = '#search=foo&map=7/4/2&redirect=/path?query=value';
hash._onHashChange();
expect(map.getZoom()).toBe(7);
expect(map.getCenter().lat).toBe(4);
expect(map.getCenter().lng).toBe(2);
});
test('hash with trailing ampersand gets removed', () => {
const hash = createHash('map')
.addTo(map);
window.location.hash = '#map=10/3/-1&foo=bar&';
hash._onHashChange();
map.setZoom(11);
expect(window.location.hash).toBe('#map=11/3/-1&foo=bar');
});
test('hash with double ampersand', () => {
const hash = createHash('map')
.addTo(map);
window.location.hash = '#map=10/3/-1&&foo=bar';
hash._onHashChange();
map.setZoom(12);
expect(window.location.hash).toBe('#map=12/3/-1&foo=bar');
});
test('hash with leading ampersand removes leading ampersand', () => {
const hash = createHash('map')
.addTo(map);
window.location.hash = '#&map=10/3/-1&foo=bar';
hash._onHashChange();
map.setZoom(13);
expect(window.location.hash).toBe('#map=13/3/-1&foo=bar');
});
test('hash with empty parameter values should be invalid', () => {
const hash = createHash('map')
.addTo(map);
window.location.hash = '#map=&foo=bar';
expect(hash._onHashChange()).toBeFalsy();
});
test('update to hash with empty parameter values is kept as-is', () => {
const hash = createHash('map')
.addTo(map);
window.location.hash = '#map=10/3/-1&empty=';
hash._onHashChange();
expect(map.getZoom()).toBe(10);
map.setZoom(5);
expect(window.location.hash).toBe('#map=5/3/-1&empty=');
});
describe('geographic boundary values', () => {
let hash: Hash;
beforeEach(() => {
hash = createHash()
.addTo(map);
});
test('Near south pole, dateline', () => {
window.location.hash = '#10/-85.05/-180';
hash._onHashChange();
expect(map.getZoom()).toBe(10);
expect(Math.abs(map.getCenter().lat)).toBeCloseTo(85.05, 1);
expect(Math.abs(map.getCenter().lng)).toBeCloseTo(180, 2);
});
test('Near north pole, positive dateline', () => {
window.location.hash = '#10/85.05/180';
hash._onHashChange();
expect(map.getZoom()).toBe(10);
expect(map.getCenter().lat).toBeCloseTo(85.05, 1);
expect(map.getCenter().lng).toBeCloseTo(180, 2);
});
test('Bearing at exact ±180° boundary', () => {
window.location.hash = '#10/0/-180/180/60';
hash._onHashChange();
expect(Math.abs(map.getCenter().lng)).toBeCloseTo(180, 2);
expect(map.getPitch()).toBe(60);
});
test('Bearing at exact -180° boundary', () => {
map.dragRotate.enable();
map.touchZoomRotate.enable();
window.location.hash = '#10/0/0/-180';
hash._onHashChange();
expect(map.getBearing()).toBe(180);
});
test('Zero zoom', () => {
window.location.hash = '#0/0/0';
hash._onHashChange();
expect(map.getZoom()).toBe(0);
});
});
test('multiple hash instances on same page', () => {
const container1 = window.document.createElement('div');
Object.defineProperty(container1, 'clientWidth', {value: 512});
Object.defineProperty(container1, 'clientHeight', {value: 512});
const map1 = globalCreateMap({container: container1});
const container2 = window.document.createElement('div');
Object.defineProperty(container2, 'clientWidth', {value: 512});
Object.defineProperty(container2, 'clientHeight', {value: 512});
const map2 = globalCreateMap({container: container2});
const hash1 = createHash('map1').addTo(map1);
const hash2 = createHash('map2').addTo(map2);
// Update first map
map1.setZoom(5);
map1.setCenter([1.0, 2.0]);
expect(window.location.hash).toBe('#map1=5/2/1');
// Update second map
map2.setZoom(10);
map2.setCenter([3.0, 4.0]);
expect(window.location.hash).toBe('#map1=5/2/1&map2=10/4/3');
// Update hash externally and verify both maps respond
window.location.hash = '#map1=7/5/6&map2=12/7/8';
hash1._onHashChange();
expect(map1.getZoom()).toBe(7);
expect(map1.getCenter().lat).toBe(5);
expect(map1.getCenter().lng).toBe(6);
hash2._onHashChange();
expect(map2.getZoom()).toBe(12);
expect(map2.getCenter().lat).toBe(7);
expect(map2.getCenter().lng).toBe(8);
// Clean up
hash1.remove();
hash2.remove();
map1.remove();
map2.remove();
});
});