mirror of
https://github.com/gramps-project/gramps-web.git
synced 2026-10-17 05:10:56 +08:00
d5e8b7d551
The Wikidata building box on the map gets a "Save as place" button that opens the new place form prefilled with the building's name, type (Building or Farm), coordinates and a Wikidata link. A building with a tree place at its coordinates shows "Open saved place" instead, which switches the panel to that place. The place box on the map shows the place type in its subtitle.
747 lines
24 KiB
JavaScript
747 lines
24 KiB
JavaScript
import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest'
|
|
|
|
import {GrampsjsViewMap} from '../../src/views/GrampsjsViewMap.js'
|
|
import {WIKIDATA_LAYER_HANDLE} from '../../src/components/GrampsjsMapWikidataLayer.js'
|
|
|
|
const PLACES = [
|
|
{handle: 'P1', profile: {name: 'Heidelberg', lat: 49.4, long: 8.7}},
|
|
{handle: 'P2', profile: {name: 'Berlin', lat: 52.5, long: 13.4}},
|
|
{handle: 'P3', profile: {name: 'Nowhere', lat: null, long: null}},
|
|
]
|
|
|
|
const PERSON = {
|
|
handle: 'I1',
|
|
profile: {name_given: 'Anna', name_surname: 'Muster'},
|
|
}
|
|
|
|
const PERSON_DATA = {
|
|
handle: 'I1',
|
|
extended: {events: [{place: 'P1'}, {place: 'P3'}, {}]},
|
|
}
|
|
|
|
const flushPromises = () => new Promise(resolve => setTimeout(resolve, 0))
|
|
|
|
function deferred() {
|
|
let resolvePromise
|
|
const promise = new Promise(resolve => {
|
|
resolvePromise = resolve
|
|
})
|
|
return {promise, resolve: resolvePromise}
|
|
}
|
|
|
|
// A map view that is never attached to the document, so nothing renders;
|
|
// the search box and the map are stubs.
|
|
function makeView() {
|
|
const view = new GrampsjsViewMap()
|
|
const map = {
|
|
flyTo: vi.fn(),
|
|
fitBounds: vi.fn(),
|
|
jumpTo: vi.fn(),
|
|
_map: {resize: vi.fn()},
|
|
}
|
|
const searchbox = {
|
|
showDetails: vi.fn(),
|
|
focus: vi.fn(),
|
|
clear: vi.fn(() => view._handleSearchClear()),
|
|
}
|
|
Object.defineProperty(view, '_mapEl', {value: map})
|
|
Object.defineProperty(view, '_searchbox', {value: searchbox})
|
|
view.appState = {
|
|
i18n: {lang: 'en', strings: {}},
|
|
apiGet: vi.fn().mockResolvedValue({data: {}}),
|
|
dbInfo: {},
|
|
}
|
|
view._dataPlaces = PLACES
|
|
view._filteredPlaces = PLACES
|
|
return {view, map, searchbox}
|
|
}
|
|
|
|
async function selectLoadedPerson(view) {
|
|
view.appState.apiGet.mockResolvedValue({data: PERSON_DATA})
|
|
view._handlePersonSelected(PERSON)
|
|
await flushPromises()
|
|
}
|
|
|
|
beforeEach(() => {
|
|
localStorage.removeItem('grampsjs_map_viewport')
|
|
localStorage.removeItem('grampsjs_map_layers')
|
|
})
|
|
|
|
describe('map view: places', () => {
|
|
it('selects a place from the search, highlights it and flies there', () => {
|
|
const {view, map, searchbox} = makeView()
|
|
view._handlePlaceSelected(PLACES[0])
|
|
expect(view._selection).to.deep.equal({
|
|
type: 'place',
|
|
handle: 'P1',
|
|
name: 'Heidelberg',
|
|
})
|
|
expect(view._highlightedHandles).to.deep.equal(['P1'])
|
|
expect(view._valueSearch).to.equal('Heidelberg')
|
|
expect(searchbox.showDetails).toHaveBeenCalled()
|
|
expect(map.flyTo).toHaveBeenCalledWith(49.4, 8.7)
|
|
})
|
|
|
|
it('selects a clicked marker without flying', () => {
|
|
const {view, map} = makeView()
|
|
view._handleMapMarkerClicked({detail: {handle: 'P2'}})
|
|
expect(view._selection).to.deep.equal({
|
|
type: 'place',
|
|
handle: 'P2',
|
|
name: 'Berlin',
|
|
})
|
|
expect(map.flyTo).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('shows a selected place that the time filter hides', () => {
|
|
const {view} = makeView()
|
|
view._filteredPlaces = [PLACES[1]]
|
|
view._handlePlaceSelected(PLACES[0])
|
|
expect(view._placesForMap.map(p => p.handle)).to.have.members(['P1', 'P2'])
|
|
})
|
|
|
|
it('selects a place opened from another page', async () => {
|
|
const {view} = makeView()
|
|
view._handleExternalPlaceSelected({detail: PLACES[1]})
|
|
await vi.waitFor(() =>
|
|
expect(view._selection).to.deep.equal({
|
|
type: 'place',
|
|
handle: 'P2',
|
|
name: 'Berlin',
|
|
})
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('map view: place map files', () => {
|
|
const KML_MEDIA = {handle: 'M1', mime: 'application/vnd.google-earth.kml+xml'}
|
|
const IMAGE_MEDIA = {handle: 'M2', mime: 'image/jpeg'}
|
|
const GEOJSON = {
|
|
type: 'Feature',
|
|
geometry: {
|
|
type: 'Polygon',
|
|
coordinates: [
|
|
[
|
|
[8, 49],
|
|
[9, 49],
|
|
[9, 50],
|
|
[8, 49],
|
|
],
|
|
],
|
|
},
|
|
properties: {},
|
|
}
|
|
const KML = `<?xml version="1.0"?>
|
|
<kml xmlns="http://www.opengis.net/kml/2.2"><Document><Placemark>
|
|
<name>Parish</name>
|
|
<LineString><coordinates>8,49 9,50</coordinates></LineString>
|
|
</Placemark></Document></kml>`
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
function stubPlace(view, media) {
|
|
view.appState.apiGet.mockResolvedValue({data: {extended: {media}}})
|
|
}
|
|
|
|
function stubFiles(files) {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn(async url => {
|
|
const handle = Object.keys(files).find(h => url.includes(`/${h}/`))
|
|
return handle
|
|
? {ok: true, text: async () => files[handle]}
|
|
: {ok: false, text: async () => ''}
|
|
})
|
|
)
|
|
}
|
|
|
|
it('draws the KML files of the selected place', async () => {
|
|
const {view, map} = makeView()
|
|
stubPlace(view, [KML_MEDIA, IMAGE_MEDIA, KML_MEDIA])
|
|
stubFiles({M1: KML})
|
|
view._handlePlaceSelected(PLACES[0])
|
|
await vi.waitFor(() => expect(view._placeVectors.layers[0]?.data).to.be.ok)
|
|
expect(view._placeVectors.layers.map(l => l.handle)).to.deep.equal(['M1'])
|
|
expect(
|
|
view._placeVectors.layers[0].data.features[0].properties.name
|
|
).to.equal('Parish')
|
|
// A place with coordinates is flown to; its files don't move the map.
|
|
expect(map.flyTo).toHaveBeenCalledWith(49.4, 8.7)
|
|
expect(map.fitBounds).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('selects a place without coordinates and fits its files', async () => {
|
|
const {view, map} = makeView()
|
|
const geojson = {handle: 'M3', mime: 'application/json', path: 'p.geojson'}
|
|
stubPlace(view, [geojson])
|
|
stubFiles({M3: JSON.stringify(GEOJSON)})
|
|
view._handlePlaceSelected(PLACES[2])
|
|
await vi.waitFor(() => expect(map.fitBounds).toHaveBeenCalled())
|
|
expect(map.fitBounds).toHaveBeenCalledWith([
|
|
[8, 49],
|
|
[9, 50],
|
|
])
|
|
expect(map.flyTo).not.toHaveBeenCalled()
|
|
expect(view._selection).to.deep.equal({
|
|
type: 'place',
|
|
handle: 'P3',
|
|
name: 'Nowhere',
|
|
})
|
|
})
|
|
|
|
it('drops the files when the selection changes', async () => {
|
|
const {view} = makeView()
|
|
stubPlace(view, [KML_MEDIA])
|
|
stubFiles({M1: KML})
|
|
view._handlePlaceSelected(PLACES[0])
|
|
await vi.waitFor(() => expect(view._placeVectors.layers[0]?.data).to.be.ok)
|
|
view._handleSearchClear()
|
|
expect(view._placeVectors.layers).to.deep.equal([])
|
|
})
|
|
|
|
it('ignores files that arrive after another selection', async () => {
|
|
const {view} = makeView()
|
|
const request = deferred()
|
|
view.appState.apiGet.mockReturnValueOnce(request.promise)
|
|
stubFiles({M1: KML})
|
|
view._handlePlaceSelected(PLACES[0])
|
|
view._handlePlaceSelected(PLACES[1])
|
|
request.resolve({data: {extended: {media: [KML_MEDIA]}}})
|
|
await flushPromises()
|
|
expect(view._selection.handle).to.equal('P2')
|
|
expect(view._placeVectors.layers).to.deep.equal([])
|
|
})
|
|
|
|
it('lists the files in the layer switcher while loading', async () => {
|
|
const {view} = makeView()
|
|
stubPlace(view, [{...KML_MEDIA, desc: 'Parish boundary'}])
|
|
const download = deferred()
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn(() => download.promise)
|
|
)
|
|
view._handlePlaceSelected(PLACES[0])
|
|
await vi.waitFor(() =>
|
|
expect(view._placeVectors.layers[0]?.loading).to.equal(true)
|
|
)
|
|
const ownOverlays = () =>
|
|
view._getOverlaysForLayerSwitcher().filter(o => o.group === 'own')
|
|
expect(ownOverlays()).to.deep.equal([
|
|
{
|
|
handle: 'M1',
|
|
desc: 'Parish boundary',
|
|
visible: true,
|
|
group: 'own',
|
|
thumbnail: {handle: 'M1', checksum: undefined, mime: KML_MEDIA.mime},
|
|
status: 'Loading...',
|
|
},
|
|
])
|
|
download.resolve({ok: true, text: async () => KML})
|
|
await vi.waitFor(() => expect(view._placeVectors.layers[0].data).to.be.ok)
|
|
expect(ownOverlays()[0].status).to.equal('')
|
|
})
|
|
|
|
it('downloads a hidden file only once it is switched on', async () => {
|
|
const {view} = makeView()
|
|
view._hiddenOverlaysHandles = ['M1']
|
|
stubPlace(view, [KML_MEDIA])
|
|
stubFiles({M1: KML})
|
|
view._handlePlaceSelected(PLACES[0])
|
|
await vi.waitFor(() => expect(view._placeVectors.layers).to.have.length(1))
|
|
await flushPromises()
|
|
expect(fetch).not.toHaveBeenCalled()
|
|
expect(view._getOverlaysForLayerSwitcher()[0].visible).to.equal(false)
|
|
|
|
view._handleOverlayToggle({
|
|
detail: {overlay: {handle: 'M1'}, visible: true},
|
|
})
|
|
await vi.waitFor(() => expect(view._placeVectors.layers[0].data).to.be.ok)
|
|
expect(fetch).toHaveBeenCalledTimes(1)
|
|
expect(view._hiddenOverlaysHandles).to.deep.equal([])
|
|
})
|
|
|
|
it('remembers a hidden file', () => {
|
|
const {view} = makeView()
|
|
view._handleOverlayToggle({
|
|
detail: {overlay: {handle: 'M1'}, visible: false},
|
|
})
|
|
expect(new GrampsjsViewMap()._hiddenOverlaysHandles).to.include('M1')
|
|
})
|
|
|
|
it('does not fit a place without coordinates to hidden files', async () => {
|
|
const {view, map} = makeView()
|
|
view._hiddenOverlaysHandles = ['M1']
|
|
stubPlace(view, [KML_MEDIA])
|
|
stubFiles({M1: KML})
|
|
view._handlePlaceSelected(PLACES[2])
|
|
await vi.waitFor(() => expect(view._placeVectors.layers).to.have.length(1))
|
|
await flushPromises()
|
|
expect(map.fitBounds).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('closes the selection when the place no longer exists', async () => {
|
|
const {view, searchbox} = makeView()
|
|
view.appState.apiGet.mockResolvedValue({
|
|
error: 'Not found',
|
|
errorDetail: {status: 404},
|
|
})
|
|
view._handlePlaceSelected(PLACES[0])
|
|
await flushPromises()
|
|
expect(searchbox.clear).toHaveBeenCalled()
|
|
expect(view._selection).to.equal(null)
|
|
})
|
|
|
|
it('keeps the selection when the place fails to load', async () => {
|
|
const {view, searchbox} = makeView()
|
|
view.appState.apiGet.mockResolvedValue({
|
|
error: 'Network error',
|
|
errorDetail: {},
|
|
})
|
|
view._handlePlaceSelected(PLACES[0])
|
|
await flushPromises()
|
|
expect(searchbox.clear).not.toHaveBeenCalled()
|
|
expect(view._selection.handle).to.equal('P1')
|
|
})
|
|
|
|
it('fits a place without coordinates to a file switched on', async () => {
|
|
const {view, map} = makeView()
|
|
view._hiddenOverlaysHandles = ['M1']
|
|
stubPlace(view, [KML_MEDIA])
|
|
stubFiles({M1: KML})
|
|
view._handlePlaceSelected(PLACES[2])
|
|
await vi.waitFor(() => expect(view._placeVectors.layers).to.have.length(1))
|
|
view._handleOverlayToggle({
|
|
detail: {overlay: {handle: 'M1'}, visible: true},
|
|
})
|
|
await vi.waitFor(() => expect(map.fitBounds).toHaveBeenCalled())
|
|
expect(map.fitBounds).toHaveBeenCalledWith([
|
|
[8, 49],
|
|
[9, 50],
|
|
])
|
|
})
|
|
|
|
it('labels a file without description by its Gramps ID', async () => {
|
|
const {view} = makeView()
|
|
stubPlace(view, [{...KML_MEDIA, desc: '', gramps_id: 'O0001'}])
|
|
stubFiles({M1: KML})
|
|
view._handlePlaceSelected(PLACES[0])
|
|
await vi.waitFor(() => expect(view._placeVectors.layers).to.have.length(1))
|
|
expect(view._getOverlaysForLayerSwitcher()[0].desc).to.equal('O0001')
|
|
})
|
|
})
|
|
|
|
describe('map view: persons', () => {
|
|
it('shows the person first and their places once loaded', async () => {
|
|
const {view, map} = makeView()
|
|
const request = deferred()
|
|
view.appState.apiGet.mockReturnValue(request.promise)
|
|
view._handlePersonSelected(PERSON)
|
|
expect(view._selection.type).to.equal('person')
|
|
expect(view._selection.data).to.equal(null)
|
|
expect(view._valueSearch).to.equal('Anna Muster')
|
|
|
|
request.resolve({data: PERSON_DATA})
|
|
await flushPromises()
|
|
expect(view._selection.data).to.equal(PERSON_DATA)
|
|
expect(view._selection.placeHandles).to.deep.equal(['P1', 'P3'])
|
|
// The only place with coordinates is shown and flown to.
|
|
expect(view._placesForMap.map(p => p.handle)).to.deep.equal(['P1'])
|
|
expect(map.flyTo).toHaveBeenCalledWith(49.4, 8.7)
|
|
})
|
|
|
|
it('shows the person places regardless of the time filter', async () => {
|
|
const {view} = makeView()
|
|
view._filteredPlaces = []
|
|
await selectLoadedPerson(view)
|
|
expect(view._personSelection).not.to.equal(null)
|
|
expect(view._placesForMap.map(p => p.handle)).to.deep.equal(['P1'])
|
|
})
|
|
|
|
it('ignores a person response after another selection', async () => {
|
|
const {view, map} = makeView()
|
|
const request = deferred()
|
|
view.appState.apiGet.mockReturnValue(request.promise)
|
|
view._handlePersonSelected(PERSON)
|
|
view._handlePlaceSelected(PLACES[1])
|
|
request.resolve({data: PERSON_DATA})
|
|
await flushPromises()
|
|
expect(view._selection).to.deep.equal({
|
|
type: 'place',
|
|
handle: 'P2',
|
|
name: 'Berlin',
|
|
})
|
|
expect(map.flyTo).toHaveBeenCalledTimes(1)
|
|
expect(map.flyTo).toHaveBeenCalledWith(52.5, 13.4)
|
|
})
|
|
|
|
it('clears the selection when the person fails to load', async () => {
|
|
const {view} = makeView()
|
|
view.appState.apiGet.mockResolvedValue({error: 'Not found'})
|
|
view._handlePersonSelected(PERSON)
|
|
await flushPromises()
|
|
expect(view._selection).to.equal(null)
|
|
})
|
|
|
|
it('highlights the place hovered in the person panel', async () => {
|
|
const {view} = makeView()
|
|
await selectLoadedPerson(view)
|
|
view._handlePlaceActive({detail: {handle: 'P1'}})
|
|
expect(view._highlightedHandles).to.deep.equal(['P1'])
|
|
view._handlePlaceActive({detail: {}})
|
|
expect(view._highlightedHandles).to.deep.equal([])
|
|
})
|
|
|
|
it('drops the hover highlight on a new selection', async () => {
|
|
const {view} = makeView()
|
|
await selectLoadedPerson(view)
|
|
view._handlePlaceActive({detail: {handle: 'P1'}})
|
|
view._handlePlaceSelected(PLACES[1])
|
|
expect(view._hoveredPlace).to.equal('')
|
|
expect(view._highlightedHandles).to.deep.equal(['P2'])
|
|
})
|
|
|
|
it('selects a person opened from another page', async () => {
|
|
const {view} = makeView()
|
|
view.appState.apiGet.mockResolvedValue({data: PERSON_DATA})
|
|
view._handleExternalPersonSelected({detail: {person: PERSON}})
|
|
await vi.waitFor(() => expect(view._selection?.data).to.equal(PERSON_DATA))
|
|
})
|
|
})
|
|
|
|
describe('map view: other selections', () => {
|
|
it('clears the selection for a Nominatim result and flies there', () => {
|
|
const {view, map} = makeView()
|
|
view._handlePlaceSelected(PLACES[0])
|
|
view._handleExternalSelected({lat: '50.1', long: '9.2', name: 'Town'})
|
|
expect(view._selection).to.equal(null)
|
|
expect(view._valueSearch).to.equal('Town')
|
|
expect(map.flyTo).toHaveBeenLastCalledWith(50.1, 9.2)
|
|
})
|
|
|
|
it('selects a Wikidata building and replaces it with a place', () => {
|
|
const {view, searchbox} = makeView()
|
|
view._handleWikidataClicked({detail: {qid: 'Q1', label: 'Old house'}})
|
|
expect(view._selection).to.deep.equal({
|
|
type: 'wikidata',
|
|
qid: 'Q1',
|
|
label: 'Old house',
|
|
lat: null,
|
|
long: null,
|
|
})
|
|
expect(view._highlightedHandles).to.deep.equal([])
|
|
expect(searchbox.showDetails).toHaveBeenCalled()
|
|
view._handlePlaceSelected(PLACES[0])
|
|
expect(view._selection.type).to.equal('place')
|
|
})
|
|
|
|
it('selects the place created for or linked to a building', () => {
|
|
const {view, map} = makeView()
|
|
view._handleWikidataClicked({detail: {qid: 'Q1', label: 'Old house'}})
|
|
view._handleWikidataPlaceSelected({
|
|
detail: {handle: 'P1', name: 'Old house'},
|
|
})
|
|
expect(view._selection).to.deep.equal({
|
|
type: 'place',
|
|
handle: 'P1',
|
|
name: 'Heidelberg',
|
|
})
|
|
expect(map.flyTo).not.toHaveBeenCalled()
|
|
view._handleWikidataPlaceSelected({detail: {handle: 'NEW', name: 'Mill'}})
|
|
expect(view._selection).to.deep.equal({
|
|
type: 'place',
|
|
handle: 'NEW',
|
|
name: 'Mill',
|
|
})
|
|
})
|
|
|
|
it('finds the place at a selected building', () => {
|
|
const {view} = makeView()
|
|
view._wikidata.buildings = [
|
|
{qid: 'Q1', label: 'Old house', lat: 49.4, long: 8.7},
|
|
{qid: 'Q2', label: 'New house', lat: 49.5, long: 8.7},
|
|
]
|
|
view._handleWikidataClicked({detail: {qid: 'Q1', label: 'Old house'}})
|
|
expect(view._placeAtBuilding(view._selection)).to.equal('P1')
|
|
view._handleWikidataClicked({detail: {qid: 'Q2', label: 'New house'}})
|
|
expect(view._placeAtBuilding(view._selection)).to.equal('')
|
|
view._handleWikidataClicked({detail: {qid: 'Q3', label: 'Not loaded'}})
|
|
expect(view._placeAtBuilding(view._selection)).to.equal('')
|
|
})
|
|
|
|
it('closes a selected building when its layer is switched off', () => {
|
|
const {view, searchbox} = makeView()
|
|
view._handleWikidataClicked({detail: {qid: 'Q1', label: 'Old house'}})
|
|
view._handleOverlayToggle({
|
|
detail: {overlay: {handle: WIKIDATA_LAYER_HANDLE}, visible: false},
|
|
})
|
|
expect(searchbox.clear).toHaveBeenCalled()
|
|
expect(view._selection).to.equal(null)
|
|
})
|
|
|
|
it('clears everything when the search is cleared', async () => {
|
|
const {view} = makeView()
|
|
await selectLoadedPerson(view)
|
|
view._handlePlaceActive({detail: {handle: 'P1'}})
|
|
view._handleSearchClear()
|
|
expect(view._selection).to.equal(null)
|
|
expect(view._highlightedHandles).to.deep.equal([])
|
|
expect(view._valueSearch).to.equal('')
|
|
})
|
|
})
|
|
|
|
describe('map view: loading places', () => {
|
|
it('centres a first visit on the places', async () => {
|
|
const {view, map} = makeView()
|
|
view._getPlaces = vi.fn().mockResolvedValue({data: PLACES})
|
|
await view._fetchPlaces()
|
|
expect(map.jumpTo).toHaveBeenCalledWith(
|
|
expect.any(Number),
|
|
expect.any(Number),
|
|
6
|
|
)
|
|
})
|
|
|
|
it('does not move the map when a place is selected', async () => {
|
|
const {view, map} = makeView()
|
|
view._handlePlaceSelected(PLACES[0])
|
|
view._getPlaces = vi.fn().mockResolvedValue({data: PLACES})
|
|
await view._fetchPlaces()
|
|
expect(map.jumpTo).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('fits the map to a loaded person when places arrive', async () => {
|
|
const {view, map} = makeView()
|
|
await selectLoadedPerson(view)
|
|
map.flyTo.mockClear()
|
|
view._getPlaces = vi.fn().mockResolvedValue({data: PLACES})
|
|
await view._fetchPlaces()
|
|
expect(map.flyTo).toHaveBeenCalledWith(49.4, 8.7)
|
|
expect(map.jumpTo).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
// A Gramps date object for 1 January of the given year.
|
|
const date = year => ({
|
|
calendar: 0,
|
|
modifier: 0,
|
|
quality: 0,
|
|
dateval: [1, 1, year, false],
|
|
sortval: 1,
|
|
text: '',
|
|
})
|
|
|
|
function withEvents(view) {
|
|
// P1 has a birth in 1850 and a death in 1950, P2 a birth in 1950, P3 none.
|
|
view._eventsByPlace = new Map([
|
|
[
|
|
'P1',
|
|
[
|
|
{place: 'P1', type: 'Birth', date: date(1850)},
|
|
{place: 'P1', type: 'Death', date: date(1950)},
|
|
],
|
|
],
|
|
['P2', [{place: 'P2', type: 'Birth', date: date(1950)}]],
|
|
])
|
|
}
|
|
|
|
// Sets the time filter as the filters panel and the slider do.
|
|
function setTimeFilter(view, year, span) {
|
|
view._handleFilterChange({detail: {timeFilter: true, yearSpan: span}})
|
|
view._handleTimeSliderChange({detail: {value: year}})
|
|
}
|
|
|
|
const handles = view => view._filteredPlaces.map(p => p.handle)
|
|
|
|
describe('map view: filters', () => {
|
|
it('starts without filters, showing all places', () => {
|
|
const {view} = makeView()
|
|
withEvents(view)
|
|
view._applyPlaceFilter()
|
|
expect(view._timeFilter).to.equal(false)
|
|
expect(handles(view)).to.deep.equal(['P1', 'P2', 'P3'])
|
|
})
|
|
|
|
it('filters places to the selected years', () => {
|
|
const {view} = makeView()
|
|
withEvents(view)
|
|
setTimeFilter(view, 1860, 25)
|
|
expect(handles(view)).to.deep.equal(['P1'])
|
|
})
|
|
|
|
it('shows all places again when the time filter is off, keeping the span', () => {
|
|
const {view} = makeView()
|
|
withEvents(view)
|
|
setTimeFilter(view, 1860, 25)
|
|
view._handleFilterChange({detail: {timeFilter: false}})
|
|
expect(handles(view)).to.deep.equal(['P1', 'P2', 'P3'])
|
|
expect(view._yearSpan).to.equal(25)
|
|
})
|
|
|
|
it('filters places by event type', () => {
|
|
const {view} = makeView()
|
|
withEvents(view)
|
|
view._handleFilterChange({detail: {eventTypes: ['Death']}})
|
|
expect(handles(view)).to.deep.equal(['P1'])
|
|
view._handleFilterChange({detail: {eventTypes: []}})
|
|
expect(handles(view)).to.deep.equal(['P1', 'P2', 'P3'])
|
|
})
|
|
|
|
it('combines the filters per event', () => {
|
|
const {view} = makeView()
|
|
withEvents(view)
|
|
// P1 has a death, and an event around 1860, but no death around 1860.
|
|
view._handleFilterChange({detail: {eventTypes: ['Death']}})
|
|
setTimeFilter(view, 1860, 25)
|
|
expect(handles(view)).to.deep.equal([])
|
|
})
|
|
|
|
it('does not apply the time filter while a person is selected', async () => {
|
|
const {view} = makeView()
|
|
setTimeFilter(view, 1860, 25)
|
|
expect(view._timeFilterActive).to.equal(true)
|
|
await selectLoadedPerson(view)
|
|
expect(view._timeFilterActive).to.equal(false)
|
|
})
|
|
|
|
it('lists the event types of the tree, most frequent first', async () => {
|
|
const {view} = makeView()
|
|
view.appState.apiGet.mockResolvedValue({
|
|
data: [
|
|
{handle: 'E1', place: 'P1', type: 'Death'},
|
|
{handle: 'E2', place: 'P1', type: 'Birth'},
|
|
{handle: 'E3', place: 'P2', type: 'Birth'},
|
|
{handle: 'E4', place: '', type: 'Census'},
|
|
],
|
|
})
|
|
await view._fetchEvents()
|
|
expect(view.appState.apiGet).toHaveBeenCalledWith(
|
|
'/api/events/?keys=date,handle,place,type'
|
|
)
|
|
expect(view._availableEventTypes).to.deep.equal([
|
|
{type: 'Birth', count: 2},
|
|
{type: 'Death', count: 1},
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('map view: time slider', () => {
|
|
it('is disabled without time filter on the base map', () => {
|
|
const {view} = makeView()
|
|
expect(view._timeSliderEnabled).to.equal(false)
|
|
})
|
|
|
|
it('is enabled while the time filter is on', () => {
|
|
const {view} = makeView()
|
|
setTimeFilter(view, 1860, 25)
|
|
expect(view._timeSliderEnabled).to.equal(true)
|
|
})
|
|
|
|
it('is enabled on the historical map', () => {
|
|
const {view} = makeView()
|
|
view._handleLayerChange({detail: {style: 'ohm'}})
|
|
expect(view._timeSliderEnabled).to.equal(true)
|
|
})
|
|
|
|
it('is disabled while a person is selected on the base map', async () => {
|
|
const {view} = makeView()
|
|
setTimeFilter(view, 1860, 25)
|
|
await selectLoadedPerson(view)
|
|
expect(view._timeSliderEnabled).to.equal(false)
|
|
})
|
|
})
|
|
|
|
describe('map view: layer settings', () => {
|
|
it('starts with the base map and the building layer off', () => {
|
|
const {view} = makeView()
|
|
expect(view._mapStyle).to.equal('base')
|
|
expect(view._wikidataVisible).to.equal(false)
|
|
})
|
|
|
|
it('remembers the building layer and the map style', () => {
|
|
const {view} = makeView()
|
|
view._handleOverlayToggle({
|
|
detail: {overlay: {handle: WIKIDATA_LAYER_HANDLE}, visible: true},
|
|
})
|
|
view._handleLayerChange({detail: {style: 'ohm'}})
|
|
|
|
const {view: reloaded} = makeView()
|
|
expect(reloaded._wikidataVisible).to.equal(true)
|
|
expect(reloaded._mapStyle).to.equal('ohm')
|
|
})
|
|
|
|
it('remembers a hidden map overlay', () => {
|
|
const {view} = makeView()
|
|
view._handleOverlayToggle({
|
|
detail: {overlay: {handle: 'M1'}, visible: false},
|
|
})
|
|
const {view: reloaded} = makeView()
|
|
expect(reloaded._hiddenOverlaysHandles).to.include('M1')
|
|
})
|
|
})
|
|
|
|
describe('map view: building status', () => {
|
|
const bounds = {
|
|
getWest: () => 8,
|
|
getSouth: () => 49,
|
|
getEast: () => 9,
|
|
getNorth: () => 50,
|
|
}
|
|
const buildings = [
|
|
{
|
|
qid: 'Q1',
|
|
lat: 49.5,
|
|
long: 8.5,
|
|
inceptionYear: 1900,
|
|
demolishedYear: null,
|
|
},
|
|
{
|
|
qid: 'Q2',
|
|
lat: 49.5,
|
|
long: 8.5,
|
|
inceptionYear: null,
|
|
demolishedYear: null,
|
|
},
|
|
{qid: 'Q3', lat: 51, long: 8.5, inceptionYear: null, demolishedYear: null},
|
|
]
|
|
|
|
function makeZoomedView() {
|
|
const {view} = makeView()
|
|
view._bounds = bounds
|
|
view._zoom = 15
|
|
view._wikidata.buildings = buildings
|
|
return view
|
|
}
|
|
|
|
it('asks to zoom in below the minimum zoom', () => {
|
|
const view = makeZoomedView()
|
|
view._zoom = 12
|
|
expect(view._wikidataStatus).to.equal('Zoom in to see Wikidata buildings')
|
|
})
|
|
|
|
it('shows that buildings are loading', () => {
|
|
const view = makeZoomedView()
|
|
view._wikidata.loading = true
|
|
expect(view._wikidataStatus).to.equal('Loading...')
|
|
})
|
|
|
|
it('shows no status while buildings are in view', () => {
|
|
const view = makeZoomedView()
|
|
expect(view._wikidataStatus).to.equal('')
|
|
})
|
|
|
|
it('says so when no building is in view', () => {
|
|
const view = makeZoomedView()
|
|
view._wikidata.buildings = [buildings[2]]
|
|
expect(view._wikidataStatus).to.equal('No buildings here')
|
|
})
|
|
|
|
it('counts only buildings matching the time filter', () => {
|
|
const view = makeZoomedView()
|
|
view._wikidata.buildings = [buildings[0]]
|
|
setTimeFilter(view, 1850, 25)
|
|
expect(view._wikidataStatus).to.equal('No buildings here')
|
|
})
|
|
})
|