Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Ensure snap points always load #193

Merged
merged 6 commits into from
Aug 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/pnpm-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ jobs:
- run: pnpm test
env:
VITE_APP_OS_PLACES_API_KEY: ${{ secrets.OS_API_KEY }}
VITE_APP_OS_VECTOR_TILES_API_KEY: ${{ secrets.OS_API_KEY }}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
"sass": "^1.44.0",
"typescript": "^4.3.5",
"vite": "^3.0.4",
"vitest": "0.20.3"
"vitest": "0.20.3",
"wait-for-expect": "^3.0.2"
},
"engines": {
"node": "^16",
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 20 additions & 16 deletions src/components/my-map/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,9 @@ export class MyMap extends LitElement {

this.map = map;

// Append to global window for reference in tests
window.olMap = import.meta.env.VITEST ? this.map : undefined;

// make configurable interactions available
const draw = configureDraw(this.drawPointer);
const modify = configureModify(this.drawPointer);
Expand Down Expand Up @@ -396,26 +399,24 @@ export class MyMap extends LitElement {
drawingLayer.setZIndex(1001);

// extract snap-able points from the basemap, and display them as points on the map if initial render within zoom
if (this.zoom < snapsZoom) {
const addSnapPoints = (): void => {
pointsSource.clear();
const extent = map.getView().calculateExtent(map.getSize());
getSnapPointsFromVectorTiles(osVectorTileBaseMap, extent);
}

// continue to fetch & update snaps as map moves
map.on("moveend", () => {
const currentZoom: number | undefined = map.getView().getZoom();
if (currentZoom && currentZoom < snapsZoom) {
pointsSource.clear();
return;
}

// timeout minimizes snap updates mid-pan/drag
setTimeout(() => {
pointsSource.clear();
if (currentZoom && currentZoom >= snapsZoom) {
const extent = map.getView().calculateExtent(map.getSize());
getSnapPointsFromVectorTiles(osVectorTileBaseMap, extent);
}, 200);
}
};

// Wait for all vector tiles to finish loading before extracting points from them
map.on("loadend", addSnapPoints);

// Update snap points when appropriate
// Timeout minimizes updates mid-pan/drag
map.on("moveend", () => {
const isSourceLoaded =
osVectorTileBaseMap.getSource()?.getState() === "ready";
if (isSourceLoaded) setTimeout(addSnapPoints, 200);
});
}

Expand Down Expand Up @@ -535,6 +536,9 @@ export class MyMap extends LitElement {
}

declare global {
interface Window {
olMap: Map | undefined;
}
interface HTMLElementTagNameMap {
"my-map": MyMap;
}
Expand Down
116 changes: 109 additions & 7 deletions src/components/my-map/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
import type { IWindow } from "happy-dom";
import { beforeEach, describe, it, expect } from "vitest";
import { beforeEach, describe, it, expect, MockInstance } from "vitest";
import Map from "ol/Map";
import { Feature } from "ol";
import Point from "ol/geom/Point";
import VectorSource from "ol/source/Vector";
import waitForExpect from "wait-for-expect";

import { getShadowRoot } from "../../test-utils";

import * as snapping from "./snapping";
import "./index";

declare global {
interface Window extends IWindow {}
}

describe("MyMap on initial render with OSM basemap", async () => {
beforeEach(async () => {
document.body.innerHTML = '<my-map id="map-vitest" disableVectorTiles />';
const setupMap = async (mapElement: any) => {
document.body.innerHTML = mapElement;
await window.happyDOM.whenAsyncComplete();
window.olMap?.dispatchEvent("loadend");
};

await window.happyDOM.whenAsyncComplete();
}, 2500);
test("olMap is added to the global window for tests", async () => {
await setupMap(`<my-map id="map-vitest" disableVectorTiles />`);
expect(window.olMap).toBeTruthy();
expect(window.olMap).toBeInstanceOf(Map);
});

describe("MyMap on initial render with OSM basemap", async () => {
beforeEach(
() => setupMap('<my-map id="map-vitest" disableVectorTiles />'),
2500
);

it("should render a custom element with a shadow root", () => {
const map = document.body.querySelector("my-map");
Expand All @@ -30,3 +46,89 @@ describe("MyMap on initial render with OSM basemap", async () => {
expect(map?.getAttribute("tabindex")).toEqual("0");
});
});

describe("Snap points loading behaviour", () => {
const ZOOM_WITHIN_RANGE = 25;
const ZOOM_OUTWITH_RANGE = 15;

const getSnapSpy: MockInstance = vi.spyOn(
snapping,
"getSnapPointsFromVectorTiles"
);
afterEach(() => {
vi.resetAllMocks();
});

it("should not load snap points if the initial zoom is not within range", async () => {
await setupMap(`<my-map
id="map-vitest"
zoom=${ZOOM_OUTWITH_RANGE}
drawMode
osVectorTileApiKey=${process.env.VITE_APP_OS_VECTOR_TILES_API_KEY}
/>`);
const pointsLayer = window.olMap
?.getAllLayers()
.find((layer) => layer.get("name") === "pointsLayer");
expect(pointsLayer).toBeDefined();
const source = pointsLayer?.getSource() as VectorSource;
expect(source.getFeatures()?.length).toEqual(0);
expect(getSnapSpy).not.toHaveBeenCalled();
});

it("should load snap points if the initial zoom is within range", async () => {
await setupMap(`<my-map
id="map-vitest"
zoom=${ZOOM_WITHIN_RANGE}
drawMode
osVectorTileApiKey=${process.env.VITE_APP_OS_VECTOR_TILES_API_KEY}
/>`);
expect(getSnapSpy).toHaveBeenCalledOnce();
});

it("should load snap points on zoom into correct range", async () => {
await setupMap(`<my-map
id="map-vitest"
zoom=${ZOOM_OUTWITH_RANGE}
drawMode
osVectorTileApiKey=${process.env.VITE_APP_OS_VECTOR_TILES_API_KEY}
/>`);
window.olMap?.getView().setZoom(ZOOM_WITHIN_RANGE);
window.olMap?.dispatchEvent("loadend");
expect(getSnapSpy).toHaveBeenCalledOnce();
});

it("should clear snap points on zoom out of range", async () => {
// Setup map and add a mock snap point
await setupMap(`<my-map
id="map-vitest"
zoom=${ZOOM_WITHIN_RANGE}
drawMode
osVectorTileApiKey=${process.env.VITE_APP_OS_VECTOR_TILES_API_KEY}
/>`);
const pointsLayer = window.olMap
?.getAllLayers()
.find((layer) => layer.get("name") === "pointsLayer");
const source = pointsLayer?.getSource() as VectorSource;
const mockSnapPoint = new Feature(new Point([1, 1]));
source?.addFeature(mockSnapPoint);
expect(source.getFeatures()?.length).toEqual(1);

// Zoom out to clear the point layer
window.olMap?.getView().setZoom(ZOOM_OUTWITH_RANGE);
window.olMap?.dispatchEvent("loadend");
expect(getSnapSpy).toHaveBeenCalledOnce();
expect(source.getFeatures()?.length).toEqual(0);
});

it("refetches snap points on a pan", async () => {
await setupMap(`<my-map
id="map-vitest"
zoom=${ZOOM_WITHIN_RANGE}
drawMode
osVectorTileApiKey=${process.env.VITE_APP_OS_VECTOR_TILES_API_KEY}
/>`);
expect(getSnapSpy).toHaveBeenCalledTimes(1);
window.olMap?.dispatchEvent("moveend");
await waitForExpect(() => expect(getSnapSpy).toHaveBeenCalledTimes(2));
});
});
3 changes: 3 additions & 0 deletions src/components/my-map/snapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export const pointsSource = new VectorSource({

export const pointsLayer = new VectorLayer({
source: pointsSource,
properties: {
name: "pointsLayer",
},
style: new Style({
image: new CircleStyle({
radius: 3,
Expand Down