UNPKG

@panoramax/web-viewer

Version:

Panoramax web viewer for geolocated pictures

908 lines (804 loc) 27.5 kB
import API from "../../src/utils/API"; global.AbortSignal = { timeout: jest.fn() }; global.console = { log: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() }; const ENDPOINT = "https://panoramax.ign.fr/api"; const VALID_LANDING = { stac_version: "1.0.0", links: [ { "rel": "data", "type": "application/rss+xml", "href": ENDPOINT+"/collections?format=rss" }, { "rel": "data", "type": "application/json", "href": ENDPOINT+"/collections" }, { "rel": "search", "type": "application/geo+json", "href": ENDPOINT+"/search" }, { "rel": "xyz", "type": "application/vnd.mapbox-vector-tile", "href": ENDPOINT+"/map/{z}/{x}/{y}.mvt" }, { "rel": "collection-preview", "type": "image/jpeg", "href": ENDPOINT+"/collections/{id}/thumb.jpg" }, { "rel": "item-preview", "type": "image/jpeg", "href": ENDPOINT+"/pictures/{id}/thumb.jpg" }, { "rel": "report", "type": "application/json", "href": ENDPOINT+"/reports" }, ], "extent": { "spatial": { "bbox": [[-0.586, 0, 6.690, 49.055]] }, "temporal": { "interval": [[ "2019-08-18T14:11:29+00:00", "2023-05-30T18:16:21.167000+00:00" ]] } } }; const LANDING_NO_PREVIEW = { stac_version: "1.0.0", links: [ { "rel": "data", "type": "application/json", "href": ENDPOINT+"/collections" }, { "rel": "search", "type": "application/geo+json", "href": ENDPOINT+"/search" }, ] }; describe("constructor", () => { // Mock landing fetch global.fetch = () => Promise.resolve({ json: () => new Promise(resolve => setTimeout(() => resolve(VALID_LANDING), 50)) }); it("works with valid endpoint", () => { const api = new API(ENDPOINT); expect(api._endpoint).toBe(ENDPOINT); }); it("works with relative path", () => { const api = new API("/api"); expect(api._endpoint).toBe("http://localhost/api"); }); it("handles tiles overrides", () => { const listener = jest.fn(); const api = new API(ENDPOINT, { tiles: "https://my.custom.tiles/" }); api.addEventListener("ready", listener); return api.onceReady().then(() => { expect(api._endpoint).toBe(ENDPOINT); expect(api._endpoints.tiles).toBe("https://my.custom.tiles/"); expect(listener).toHaveBeenCalled(); }); }); it("fails if endpoint is invalid", () => { expect(() => new API("not an url")).toThrow("endpoint parameter is not a valid URL: not an url"); }); it("fails if endpoint is empty", () => { expect(() => new API()).toThrow("endpoint parameter is empty or not a valid string"); }); it("fails on fetch failure", async () => { // Mock landing fetch global.fetch = () => Promise.resolve({ json: () => new Promise((resolve, reject) => setTimeout(() => reject(new Error("brok")), 50)) }); const listener = jest.fn(); const api = new API(ENDPOINT); api.addEventListener("broken", listener); return api.onceReady().catch(e => { expect(e).toEqual("Viewer failed to communicate with API"); expect(listener.mock.calls).toMatchSnapshot(); }); }); it("accepts fetch options", () => { const api = new API("/api", { fetch: { bla: "bla" } }); expect(api._getFetchOptions()).toEqual({ bla: "bla" }); }); }); describe("onceReady", () => { it("works if API is ready", async () => { // Mock landing fetch global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve(VALID_LANDING) })); const api = new API(ENDPOINT); const res = await api.onceReady(); expect(res).toBe("API is ready"); // Also work after initial promise resolve const res2 = await api.onceReady(); expect(res2).toBe("API is ready"); }); it("handles API failures", async () => { // Mock landing fetch fetch.mockRejectedValueOnce(); const api = new API(ENDPOINT); await expect(api.onceReady()).rejects.toBe("Viewer failed to communicate with API"); // Also work after initial promise end await expect(api.onceReady()).rejects.toBe("Viewer failed to communicate with API"); }); }); describe("isReady", () => { // Randomly fails for no reason it.skip("works if API is ready", async () => { // Mock landing fetch global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve(VALID_LANDING) })); const api = new API(ENDPOINT); await api.onceReady(); expect(api.isReady()).toBeTruthy(); }); it("works with API failing", async () => { // Mock landing fetch fetch.mockRejectedValueOnce(); const api = new API(ENDPOINT); try { return await api.onceReady(); } catch { expect(api.isReady()).toBeFalsy(); } }); }); describe("_parseLanding", () => { it("handles overrides for tiles URL", () => { const api = new API (ENDPOINT, { skipReadLanding: true }); api._parseLanding(VALID_LANDING, { tiles: "https://my.custom.tiles/" }); expect(api._endpoints.tiles).toBe("https://my.custom.tiles/"); }); it("fails if landing JSON lacks info", () => { const api = new API (ENDPOINT, { skipReadLanding: true }); expect(() => api._parseLanding({})).toThrow("API Landing page doesn't contain 'links' list"); }); it.each([ ["search", "application/geo+json"], ["data", "application/json"], ["data", "application/rss+xml"], ["xyz", "application/vnd.mapbox-vector-tile"], ["xyz-style", "application/json"], ["user-xyz", "application/vnd.mapbox-vector-tile"], ["user-xyz-style", "application/json"], ["user-search", "application/json"], ["collection-preview", "image/jpeg"], ["item-preview", "image/jpeg"], ["report", "application/json"], ])("fails if link rel=%s type=%s is invalid", (rel, type) => { const api = new API (ENDPOINT, { skipReadLanding: true }); const landing = { stac_version: "1.0.0", links: [ { "rel": rel, "href": "bla", "type": type } ] }; try { api._parseLanding(landing); throw new Error("Should not succeed"); } catch(e) { expect(e.message).toMatchSnapshot(); } }); it("fails if API version is not supported", () => { const api = new API (ENDPOINT, { skipReadLanding: true }); const landing = { stac_version: "0.1", links: [] }; expect(() => api._parseLanding(landing)).toThrow("API is not in a supported STAC version (Panoramax viewer supports only 1.x, API is 0.1)"); }); it("fails if mandatory links are not set", () => { const api = new API (ENDPOINT, { skipReadLanding: true }); const landing = { stac_version: "1.0.0", links: [] }; try { api._parseLanding(landing); throw new Error("Should not succeed"); } catch(e) { expect(e.message).toMatchSnapshot(); } }); }); describe("_loadMapStyles", () => { it("works if no background style set", async() => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(VALID_LANDING); await api._loadMapStyles(); expect(api.mapStyle).toMatchSnapshot(); }); it("loads background style from string", async () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(LANDING_NO_PREVIEW); global.fetch = () => Promise.resolve({ json: () => ({ name: "Provider", sources: { provider: {} }, layers: [{id: "provlayer"}], })}); await api._loadMapStyles("https://tiles.provider/style.json"); expect(api.mapStyle).toMatchSnapshot(); }); it("loads background style from json", async () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(LANDING_NO_PREVIEW); await api._loadMapStyles({ name: "Provider", sources: { provider: {} }, layers: [{id: "provlayer"}], }); expect(api.mapStyle).toMatchSnapshot(); }); it("handles default user", async () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(VALID_LANDING); await api._loadMapStyles(undefined, ["geovisio"]); expect(api.mapStyle).toMatchSnapshot(); }); it("handles various users", async () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding({ stac_version: "1.0.0", links: [ { "rel": "data", "type": "application/json", "href": ENDPOINT+"/collections" }, { "rel": "search", "type": "application/geo+json", "href": ENDPOINT+"/search" }, { "rel": "xyz-style", "type": "application/json", "href": ENDPOINT+"/map/style.json" }, { "rel": "user-xyz-style", "type": "application/json", "href": ENDPOINT+"/users/{userId}/map/style.json" }, ], "extent": { "spatial": { "bbox": [[-0.586, 0, 6.690, 49.055]] }, "temporal": { "interval": [[ "2019-08-18T14:11:29+00:00", "2023-05-30T18:16:21.167000+00:00" ]] } } }); global.fetch = (url) => { if(url.includes("/users") && url.includes("style.json")) { let user = null; if(url.includes("/bla/")) { user = "bla"; } if(url.includes("/blo/")) { user = "blo"; } return Promise.resolve({ json: () => Promise.resolve({ sources: { [`provider_${user}`]: {} }, layers: [{id: `provider_${user}`}], }) }); } else if(url === ENDPOINT+"/map/style.json") { return Promise.resolve({ json: () => Promise.resolve({ sources: { [`provider`]: {} }, layers: [{id: `provider`}], }) }); } }; await api._loadMapStyles(undefined, ["bla", "blo"]); expect(api.mapStyle).toMatchSnapshot(); }); }); describe("_getMapRequestTransform", () => { it("does nothing if no tiles enabled", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(LANDING_NO_PREVIEW); expect(api._getMapRequestTransform()).toBe(undefined); }); it("does nothing if no fetch options defined", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(VALID_LANDING); expect(api._getMapRequestTransform()).toBe(undefined); }); it("returns a function with correct options if fetch options defined", () => { const api = new API(ENDPOINT, { skipReadLanding: true, fetch: { credentials: "include", headers: { "Accept-Header": "Whatever" } } }); api._parseLanding(VALID_LANDING); // With tiles endpoint called const res = api._getMapRequestTransform(); const res1 = res(ENDPOINT+"/map/8/1234/4567.mvt"); expect(res1).toEqual({ url: ENDPOINT+"/map/8/1234/4567.mvt", credentials: "include", headers: { "Accept-Header": "Whatever" } }); // With external endpoint called const res2 = res("https://my-tile-provider.fr/map/1/2/3.mvt"); expect(res2).toEqual(undefined); }); }); describe("getPicturesAroundCoordinatesUrl", () => { it("works with valid coordinates", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(VALID_LANDING); api._isReady = 1; expect(api.getPicturesAroundCoordinatesUrl(48.7, -1.25)).toBe(`${ENDPOINT}/search?bbox=-1.2505,48.6995,-1.2495,48.7005`); }); it("fails if coordinates are invalid", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(VALID_LANDING); api._isReady = 1; expect(() => api.getPicturesAroundCoordinatesUrl()).toThrow("lat and lon parameters should be valid numbers"); }); }); describe("getPictureMetadataUrl", () => { it("works with valid ID", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(VALID_LANDING); api._isReady = 1; expect(api.getPictureMetadataUrl("whatever-id")).toBe(`${ENDPOINT}/search?ids=whatever-id`); }); it("works with valid ID and sequence", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(VALID_LANDING); api._isReady = 1; expect(api.getPictureMetadataUrl("whatever-id", "my-sequence")).toBe(`${ENDPOINT}/collections/my-sequence/items/whatever-id`); }); it("fails if picId is invalid", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(VALID_LANDING); api._isReady = 1; expect(() => api.getPictureMetadataUrl()).toThrow("id should be a valid picture unique identifier"); }); }); describe("getMapStyle", () => { it("sends ready mapstyle", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(VALID_LANDING); api._isReady = 1; api.mapStyle = {name: "Ready"}; expect(api.getMapStyle()).toBe(api.mapStyle); }); it("loads style from endpoint", async () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding({ stac_version: "1.0.0", links: [ { "rel": "data", "type": "application/json", "href": ENDPOINT+"/collections" }, { "rel": "search", "type": "application/geo+json", "href": ENDPOINT+"/search" }, { "rel": "xyz-style", "type": "application/json", "href": ENDPOINT+"/map/style.json" }, ], "extent": { "spatial": { "bbox": [[-0.586, 0, 6.690, 49.055]] }, "temporal": { "interval": [[ "2019-08-18T14:11:29+00:00", "2023-05-30T18:16:21.167000+00:00" ]] } } }); global.fetch = jest.fn(() => Promise.resolve({ json: () => ({ name: "Ready" }) })); const res = await api.getMapStyle(); expect(res).toStrictEqual({ name: "Ready" }); }); it("creates style from tiles endpoint", async () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(VALID_LANDING); const res = await api.getMapStyle(); expect(res).toStrictEqual({ "version": 8, "sources": { "geovisio": { "type": "vector", "tiles": [ ENDPOINT+"/map/{z}/{x}/{y}.mvt" ], "minzoom": 0, "maxzoom": 15 } } }); }); it("fallbacks to /map route if any", async () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(LANDING_NO_PREVIEW); global.fetch = jest.fn(() => Promise.resolve()); const res = await api.getMapStyle(); expect(res).toStrictEqual({ "version": 8, "sources": { "geovisio": { "type": "vector", "tiles": [ ENDPOINT+"/map/{z}/{x}/{y}.mvt" ], "minzoom": 0, "maxzoom": 15 } } }); }); it("fails if no fallback /map route", async () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(LANDING_NO_PREVIEW); global.fetch = jest.fn(() => Promise.reject()); await expect(async () => await api.getMapStyle()).rejects.toEqual(new Error("API doesn't offer a vector tiles endpoint")); }); }); describe("getUserMapStyle", () => { it("fails if not ready", async () => { const api = new API(ENDPOINT, { skipReadLanding: true }); await expect(async () => await api.getUserMapStyle("bla")).rejects.toEqual(new Error("API is not ready to use")); }); it("fails if no userId", async () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._isReady = 1; await expect(async () => await api.getUserMapStyle()).rejects.toEqual(new Error("Parameter userId is empty")); }); it("loads style from endpoint", async () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding({ stac_version: "1.0.0", links: [ { "rel": "data", "type": "application/json", "href": ENDPOINT+"/collections" }, { "rel": "search", "type": "application/geo+json", "href": ENDPOINT+"/search" }, { "rel": "user-xyz-style", "type": "application/json", "href": ENDPOINT+"/users/{userId}/map/style.json" }, ], "extent": { "spatial": { "bbox": [[-0.586, 0, 6.690, 49.055]] }, "temporal": { "interval": [[ "2019-08-18T14:11:29+00:00", "2023-05-30T18:16:21.167000+00:00" ]] } } }); api._isReady = 1; global.fetch = (url) => { expect(url).toBe(ENDPOINT+"/users/bla/map/style.json"); return Promise.resolve({ json: () => Promise.resolve({ name: "Ready" }) }); }; const res = await api.getUserMapStyle("bla"); expect(res).toStrictEqual({ name: "Ready" }); }); it("creates style from tiles endpoint", async () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding({ stac_version: "1.0.0", links: [ { "rel": "data", "type": "application/json", "href": ENDPOINT+"/collections" }, { "rel": "search", "type": "application/geo+json", "href": ENDPOINT+"/search" }, { "rel": "user-xyz", "type": "application/vnd.mapbox-vector-tile", "href": ENDPOINT+"/users/{userId}/map/{z}/{x}/{y}.mvt" }, ], "extent": { "spatial": { "bbox": [[-0.586, 0, 6.690, 49.055]] }, "temporal": { "interval": [[ "2019-08-18T14:11:29+00:00", "2023-05-30T18:16:21.167000+00:00" ]] } } }); api._isReady = 1; const res = await api.getUserMapStyle("bla"); expect(res).toStrictEqual({ "version": 8, "sources": { "geovisio_bla": { "type": "vector", "tiles": [ ENDPOINT+"/users/bla/map/{z}/{x}/{y}.mvt" ], "minzoom": 0, "maxzoom": 15 } } }); }); it("fails if no style found", async () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(LANDING_NO_PREVIEW); api._isReady = 1; await expect(async () => await api.getUserMapStyle("bla")).rejects.toEqual(new Error("API doesn't offer map style for specific user")); }); }); describe("findThumbnailInPictureFeature", () => { it("works if a thumbnail exists", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(VALID_LANDING); api._isReady = 1; const res = api.findThumbnailInPictureFeature({ assets: { t: { roles: ["thumbnail"], type: "image/jpeg", href: "https://geovisio.fr/thumb.jpg" } } }); expect(res).toEqual("https://geovisio.fr/thumb.jpg"); }); it("works if a visual exists", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(VALID_LANDING); api._isReady = 1; const res = api.findThumbnailInPictureFeature({ assets: { t: { roles: ["visual"], type: "image/jpeg", href: "https://geovisio.fr/thumb.jpg" } } }); expect(res).toEqual("https://geovisio.fr/thumb.jpg"); }); it("works if no thumbnail is found", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(VALID_LANDING); api._isReady = 1; const res = api.findThumbnailInPictureFeature({}); expect(res).toBe(null); }); }); describe("getPictureThumbnailURLForSequence", () => { it("works with a collection-preview endpoint", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(VALID_LANDING); api._isReady = 1; return api.getPictureThumbnailURLForSequence("12345").then(url => { expect(url).toBe(ENDPOINT+"/collections/12345/thumb.jpg"); }); }); it("works if a preview is defined in sequence metadata", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(LANDING_NO_PREVIEW); api._isReady = 1; const seq = { links: [ { "type": "image/jpeg", "rel": "preview", "href": "https://geovisio.fr/preview/thumb.jpg" } ] }; return api.getPictureThumbnailURLForSequence("12345", seq).then(url => { expect(url).toBe("https://geovisio.fr/preview/thumb.jpg"); }); }); it("works with an existing sequence", () => { const resPicId = "cbfc3add-8173-4464-98c8-de2a43c6a50f"; const thumbUrl = "http://my.custom.api/pic/thumb.jpg"; // Mock API search global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve({ features: [ { "id": resPicId, "assets": { "thumb": { "href": thumbUrl, "roles": ["thumbnail"], "type": "image/jpeg" } } }] }) })); const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(LANDING_NO_PREVIEW); api._isReady = 1; return api.getPictureThumbnailURLForSequence("208b981a-262e-4966-97b6-98ee0ceb8df0").then(url => { expect(url).toBe(thumbUrl); }); }); it("works with no results", () => { // Mock API search global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve({ features: [] }) })); const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(LANDING_NO_PREVIEW); api._isReady = 1; return api.getPictureThumbnailURLForSequence("208b981a-262e-4966-97b6-98ee0ceb8df0").then(url => { expect(url).toBe(null); }); }); }); describe("getPictureThumbnailURL", () => { it("works with a item-preview endpoint", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(VALID_LANDING); api._isReady = 1; return api.getPictureThumbnailURL("12345").then(url => { expect(url).toBe(ENDPOINT+"/pictures/12345/thumb.jpg"); }); }); it("works with picture and sequence ID defined", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(LANDING_NO_PREVIEW); api._isReady = 1; // Mock API search global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve({ "assets": { "thumb": { "href": ENDPOINT+"/pictures/pic1/thumb.jpg", "roles": ["thumbnail"], "type": "image/jpeg" } } }) })); return api.getPictureThumbnailURL("pic1", "seq1").then(url => { expect(url).toBe(ENDPOINT+"/pictures/pic1/thumb.jpg"); }); }); it("works with picture and sequence ID defined, but no thumb found", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(LANDING_NO_PREVIEW); api._isReady = 1; // Mock API search global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve({}) })); return api.getPictureThumbnailURL("pic1", "seq1").then(url => { expect(url).toBe(null); }); }); it("works with picture ID defined", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(LANDING_NO_PREVIEW); api._isReady = 1; // Mock API search global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve({ features: [{ "assets": { "thumb": { "href": ENDPOINT+"/pictures/pic1/thumb.jpg", "roles": ["thumbnail"], "type": "image/jpeg" } } }] }) })); return api.getPictureThumbnailURL("pic1").then(url => { expect(url).toBe(ENDPOINT+"/pictures/pic1/thumb.jpg"); }); }); it("works with picture ID defined but no results", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(LANDING_NO_PREVIEW); api._isReady = 1; // Mock API search global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve({ features: [] }) })); return api.getPictureThumbnailURL("pic1").then(url => { expect(url).toBe(null); }); }); }); describe("getRSSURL", () => { it("works without RSS", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(LANDING_NO_PREVIEW); api._isReady = 1; expect(api.getRSSURL()).toBeNull(); }); it("works with RSS and no bbox", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(VALID_LANDING); api._isReady = 1; expect(api.getRSSURL()).toBe(ENDPOINT+"/collections?format=rss"); }); it("works with RSS and bbox with query string", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(VALID_LANDING); api._isReady = 1; const bbox = { getSouth: () => -1.7, getNorth: () => -1.6, getWest: () => 47.1, getEast: () => 48.2 }; expect(api.getRSSURL(bbox)).toBe(ENDPOINT+"/collections?format=rss&bbox=47.1,-1.7,48.2,-1.6"); }); it("works with RSS and bbox without query string", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding({ stac_version: "1.0.0", links: [ { "rel": "data", "type": "application/json", "href": ENDPOINT+"/collections" }, { "rel": "search", "type": "application/geo+json", "href": ENDPOINT+"/search" }, { "rel": "data", "href": ENDPOINT+"/collections", "type": "application/rss+xml" } ] }); api._isReady = 1; const bbox = { getSouth: () => -1.7, getNorth: () => -1.6, getWest: () => 47.1, getEast: () => 48.2 }; expect(api.getRSSURL(bbox)).toBe(ENDPOINT+"/collections?bbox=47.1,-1.7,48.2,-1.6"); }); }); describe("getSequenceMetadataUrl", () => { it("works", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(VALID_LANDING); api._isReady = 1; expect(api.getSequenceMetadataUrl("blabla")).toBe(ENDPOINT+"/collections/blabla"); }); }); describe("getDataBbox", () => { it("works with landing spatial extent defined", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(VALID_LANDING); api._isReady = 1; expect(api.getDataBbox()).toEqual([[-0.586, 0], [6.690, 49.055]]); }); it("works with no landing spatial extent defined", () => { const api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(LANDING_NO_PREVIEW); api._isReady = 1; expect(api.getDataBbox()).toBe(null); }); }); describe("sendReport", () => { let api; beforeEach(() => { api = new API(ENDPOINT, { skipReadLanding: true }); api._parseLanding(VALID_LANDING); api.isReady = () => true; }); it("throws an error if API is not ready", () => { api.isReady = () => false; expect(() => api.sendReport({})).toThrow("API is not ready to use"); }); it("throws an error if report endpoint is not available", () => { api._endpoints.report = null; expect(() => api.sendReport({})).toThrow("Report sending is not available"); }); it("sends a report successfully", async () => { // Mock fetch response const mockResponse = { status: 200, json: jest.fn().mockResolvedValue({ id: "bla" }) }; global.fetch = jest.fn().mockResolvedValue(mockResponse); const data = { issue: "blur_missing", picture_id: "bla1", sequence_id: "bla2", }; const response = await api.sendReport(data); expect(fetch).toHaveBeenCalledWith(ENDPOINT+"/reports", { method: "POST", body: JSON.stringify(data), headers: { "Content-Type": "application/json" } }); expect(response).toEqual({ id: "bla" }); }); it("handles API errors and rejects with message", async () => { // Mock fetch response with an error const mockResponse = { status: 400, text: jest.fn().mockResolvedValue(JSON.stringify({ message: "Error occurred" })) }; global.fetch = jest.fn().mockResolvedValue(mockResponse); const data = { issue: "blur_missing", picture_id: "bla1", sequence_id: "bla2", }; await expect(api.sendReport(data)).rejects.toEqual("Error occurred"); expect(fetch).toHaveBeenCalledWith(ENDPOINT+"/reports", { method: "POST", body: JSON.stringify(data), headers: { "Content-Type": "application/json" } }); }); it("handles API errors and rejects with text if no message in JSON", async () => { // Mock fetch response with an error and no "message" key const mockResponse = { status: 400, text: jest.fn().mockResolvedValue("Some error text") }; global.fetch = jest.fn().mockResolvedValue(mockResponse); const data = { issue: "blur_missing", picture_id: "bla1", sequence_id: "bla2", }; await expect(api.sendReport(data)).rejects.toEqual("Some error text"); expect(fetch).toHaveBeenCalledWith(ENDPOINT+"/reports", { method: "POST", body: JSON.stringify(data), headers: { "Content-Type": "application/json" } }); }); }); describe("isValidHttpUrl", () => { it("works with valid endpoint", () => { expect(API.isValidHttpUrl(ENDPOINT)).toBeTruthy(); }); it("fails if endpoint is invalid", () => { expect(API.isValidHttpUrl("not an url")).toBeFalsy(); }); }); describe("isValidId", () => { it("works with valid ID", () => { expect(API.isIdValid("blabla")).toBeTruthy(); }); it("fails with invalid ID", () => { expect(() => API.isIdValid(null)).toThrowError("id should be a valid picture unique identifier"); }); });