diff --git a/src/ui/hash.test.ts b/src/ui/hash.test.ts index c08a4726bd0..251ff3c5db7 100644 --- a/src/ui/hash.test.ts +++ b/src/ui/hash.test.ts @@ -63,7 +63,7 @@ describe('hash', () => { expect(map.getCenter().lng).toBe(-1); expect(map.getCenter().lat).toBe(3); expect(map.getZoom()).toBe(10); - expect(map.getBearing() === 0 ? 0 : map.getBearing()).toBe(0); + expect(map.getBearing()).toBe(0); expect(map.getPitch()).toBe(0); // map is created with `interactive: false` @@ -124,7 +124,7 @@ describe('hash', () => { expect(map.getCenter().lng).toBe(-1); expect(map.getCenter().lat).toBe(3); expect(map.getZoom()).toBe(10); - expect(map.getBearing() === 0 ? 0 : map.getBearing()).toBe(0); + expect(map.getBearing()).toBe(0); expect(map.getPitch()).toBe(0); window.location.hash = ''; @@ -134,7 +134,7 @@ describe('hash', () => { expect(map.getCenter().lng).toBe(-1); expect(map.getCenter().lat).toBe(3); expect(map.getZoom()).toBe(10); - expect(map.getBearing() === 0 ? 0 : map.getBearing()).toBe(0); + expect(map.getBearing()).toBe(0); expect(map.getPitch()).toBe(0); }); @@ -149,7 +149,7 @@ describe('hash', () => { expect(map.getCenter().lng).toBe(-1); expect(map.getCenter().lat).toBe(3); expect(map.getZoom()).toBe(10); - expect(map.getBearing() === 0 ? 0 : map.getBearing()).toBe(0); + expect(map.getBearing()).toBe(0); expect(map.getPitch()).toBe(0); window.location.hash = '#map&foo=bar'; @@ -170,12 +170,7 @@ describe('hash', () => { .addTo(map); window.location.hash = '#10/3.00/-1.00'; - - const currentHash = hash._getCurrentHash(); - - expect(currentHash[0]).toBe('10'); - expect(currentHash[1]).toBe('3.00'); - expect(currentHash[2]).toBe('-1.00'); + expect(hash._getCurrentHash()).toStrictEqual(['10', '3.00', '-1.00']); }); test('_getCurrentHash named', () => { @@ -183,27 +178,50 @@ describe('hash', () => { .addTo(map); window.location.hash = '#map=10/3.00/-1.00&foo=bar'; + expect(hash._getCurrentHash()).toStrictEqual(['10', '3.00', '-1.00']); - let currentHash = hash._getCurrentHash(); + window.location.hash = '#baz&map=10/3.00/-1.00'; + expect(hash._getCurrentHash()).toStrictEqual(['10', '3.00', '-1.00']); + }); - expect(currentHash[0]).toBe('10'); - expect(currentHash[1]).toBe('3.00'); - expect(currentHash[2]).toBe('-1.00'); + describe('getHashString', () => { + let hash: Hash; - window.location.hash = '#baz&map=10/3.00/-1.00'; + beforeEach(() => { + hash = createHash() + .addTo(map); + }); + + test('mapFeedback=true', () => { + map.setZoom(10); + map.setCenter([2.5, 3.75]); - currentHash = hash._getCurrentHash(); + const hashStringWithFeedback = hash.getHashString(true); + expect(hashStringWithFeedback).toBe('#/2.5/3.75/10'); - expect(currentHash[0]).toBe('10'); - expect(currentHash[1]).toBe('3.00'); - expect(currentHash[2]).toBe('-1.00'); + 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', () => { - function getHash() { - return window.location.hash.split('/'); - } - createHash() .addTo(map); @@ -212,36 +230,15 @@ describe('hash', () => { map.setZoom(3); map.setCenter([2.0, 1.0]); - expect(window.location.hash).toBeTruthy(); - - let newHash = getHash(); - - expect(newHash).toHaveLength(3); - expect(newHash[0]).toBe('#3'); - expect(newHash[1]).toBe('1'); - expect(newHash[2]).toBe('2'); + expect(window.location.hash).toBe('#3/1/2'); map.setPitch(60); - newHash = getHash(); - - expect(newHash).toHaveLength(5); - expect(newHash[0]).toBe('#3'); - expect(newHash[1]).toBe('1'); - expect(newHash[2]).toBe('2'); - expect(newHash[3]).toBe('0'); - expect(newHash[4]).toBe('60'); + expect(window.location.hash).toBe('#3/1/2/0/60'); map.setBearing(135); - newHash = getHash(); - - expect(newHash).toHaveLength(5); - expect(newHash[0]).toBe('#3'); - expect(newHash[1]).toBe('1'); - expect(newHash[2]).toBe('2'); - expect(newHash[3]).toBe('135'); - expect(newHash[4]).toBe('60'); + expect(window.location.hash).toBe('#3/1/2/135/60'); }); test('_updateHash named', () => { @@ -279,50 +276,67 @@ describe('hash', () => { expect(window.location.hash).toBe('#baz&map=7/1/2/135/60&foo=bar'); }); - test('_removeHash', () => { - const hash = createHash() - .addTo(map); + describe('_removeHash without a name', () => { + let hash: Hash; - map.setZoom(3); - map.setCenter([2.0, 1.0]); + beforeEach(() => { + hash = createHash() + .addTo(map); + }); - expect(window.location.hash).toBe('#3/1/2'); + test('removes hash when hash is only map hash', () => { + map.setZoom(3); + map.setCenter([2.0, 1.0]); - hash._removeHash(); + expect(window.location.hash).toBe('#3/1/2'); - expect(window.location.hash).toBe(''); + hash._removeHash(); + + expect(window.location.hash).toBe(''); + }); - window.location.hash = '#3/1/2&foo=bar'; + test('removes hash when hash contains other parameters', () => { + window.location.hash = '#3/1/2&foo=bar'; - hash._removeHash(); + hash._removeHash(); - expect(window.location.hash).toBe('#foo=bar'); + expect(window.location.hash).toBe('#foo=bar'); + }); }); - test('_removeHash named', () => { - const hash = createHash('map') - .addTo(map); + describe('_removeHash with a name', () => { + let hash: Hash; - map.setZoom(3); - map.setCenter([2.0, 1.0]); + beforeEach(() => { + hash = createHash('map') + .addTo(map); + }); - expect(window.location.hash).toBe('#map=3/1/2'); + 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(); + hash._removeHash(); - expect(window.location.hash).toBe(''); + expect(window.location.hash).toBe(''); + }); - window.location.hash = '#map=3/1/2&foo=bar'; + test('removes hash when hash contains other parameters at end', () => { + window.location.hash = '#map=3/1/2&foo=bar'; - hash._removeHash(); + hash._removeHash(); - expect(window.location.hash).toBe('#foo=bar'); + expect(window.location.hash).toBe('#foo=bar'); + }); - window.location.hash = '#baz&map=7/2/1/135/60&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(); + hash._removeHash(); - expect(window.location.hash).toBe('#baz&foo=bar'); + expect(window.location.hash).toBe('#baz&foo=bar'); + }); }); describe('_isValidHash', () => { @@ -359,6 +373,18 @@ describe('hash', () => { 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'; @@ -386,80 +412,269 @@ describe('hash', () => { expect(hash._isValidHash(hash._getCurrentHash())).toBeFalsy(); }); + + test('invalidate hash, slashes encoded as %2F are not recognised', () => { + window.location.hash = '#10%2F3.00%2F-1.00'; + + expect(hash._isValidHash(hash._getCurrentHash())).toBeFalsy(); + }); }); - test('initialize 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'); + 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('initialize 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('map.remove', () => { + const container = window.document.createElement('div'); + Object.defineProperty(container, 'clientWidth', {value: 512}); + Object.defineProperty(container, 'clientHeight', {value: 512}); + + map.remove(); + }); - test('initialize 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('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('initialize http://localhost/', () => { - window.location.href = 'http://localhost/'; - createHash().addTo(map); + 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); - 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'); + 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('initialize default value for window.location.href', () => { - createHash().addTo(map); + 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('#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'); + expect(window.location.hash).toBe('#map=5/3/-1&empty='); }); - test('initialize 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'); + 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('map.remove', () => { - const container = window.document.createElement('div'); - Object.defineProperty(container, 'clientWidth', {value: 512}); - Object.defineProperty(container, 'clientHeight', {value: 512}); + 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}); - map.remove(); + 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); - expect(map).toBeTruthy(); + // Clean up + hash1.remove(); + hash2.remove(); + map1.remove(); + map2.remove(); }); });