import { describe, expect, it } from "vitest"; import { MAX_FIT_ZOOM } from "@/lib/basemap"; import { LABEL_LANGUAGES, parsePayload, parseStyleRequest, PayloadError, } from "./payload"; const TILE = { kind: "tile", theme: "light", z: 9, x: 300, y: 215 }; function rejects(body: unknown, message: RegExp) { expect(() => parsePayload(body)).toThrow(PayloadError); expect(() => parsePayload(body)).toThrow(message); } describe("parsePayload", () => { it("takes a tile in either theme", () => { expect(parsePayload(TILE)).toEqual(TILE); expect(parsePayload({ ...TILE, theme: "dark" })).toEqual({ ...TILE, theme: "dark", }); }); it("takes the corners of the grid at every zoom it draws", () => { for (let z = 0; z <= MAX_FIT_ZOOM; z += 1) { const last = 2 ** z - 1; expect(parsePayload({ ...TILE, z, x: 0, y: 0 })).toMatchObject({ z }); expect(parsePayload({ ...TILE, z, x: last, y: last })).toMatchObject({ x: last, y: last, }); } }); // The deepest any of the web's maps is fitted at, and the API's ceiling too. it("refuses a zoom past the deepest a map is fitted at", () => { expect(MAX_FIT_ZOOM).toBe(9); rejects({ ...TILE, z: 10, x: 0, y: 0 }, /z must be from 0 to 9/); rejects({ ...TILE, z: -1 }, /z must be from 0 to 9/); }); it("refuses a square outside the grid", () => { rejects({ ...TILE, x: 512 }, /x must be from 0 to 511/); rejects({ ...TILE, y: 512 }, /y must be from 0 to 511/); rejects({ ...TILE, z: 0, x: 1, y: 0 }, /x must be from 0 to 0/); rejects({ ...TILE, x: -1 }, /x must be from 0/); }); it("refuses coordinates that are not integers", () => { rejects({ ...TILE, x: 1.5 }, /x must be an integer/); rejects({ ...TILE, z: "9" }, /z must be an integer/); rejects({ ...TILE, y: null }, /y must be an integer/); }); it("refuses a body naming a record's places", () => { rejects( { kind: "dive", theme: "light", dive_sites: [{ latitude: 28.5721, longitude: 34.5372 }], entry_latitude: null, entry_longitude: null, exit_latitude: null, exit_longitude: null, }, /missing z, x, y/, ); rejects({ ...TILE, kind: "trip" }, /kind must be "tile"/); }); it("refuses a missing key and an extra one alike", () => { const { y: _y, ...missing } = TILE; rejects(missing, /missing y/); rejects({ ...TILE, account: "someone" }, /unexpected account/); }); it("refuses a theme it does not draw, and a body that is not an object", () => { rejects({ ...TILE, theme: "sepia" }, /theme/); rejects([TILE], /object/); rejects(null, /object/); }); it("takes a language from the list, and leaves a tile without one in English", () => { expect(parsePayload({ ...TILE, language: "de" })).toEqual({ ...TILE, language: "de", }); expect(parsePayload({ ...TILE, language: "pnb" })).toMatchObject({ language: "pnb", }); expect("language" in parsePayload(TILE)).toBe(false); }); // The API reduces what a browser says to a code of the list or to no key at // all, so anything else here is a client that skipped that. it.each([["en"], ["de-DE"], ["DE"], [""], [null], [7], [["de"]]])( "refuses the language %j", (language) => { rejects({ ...TILE, language }, /language must be one of/); }, ); }); describe("parseStyleRequest", () => { it("takes either theme, with no language or one of the list", () => { expect(parseStyleRequest("light", null)).toEqual({ theme: "light" }); expect(parseStyleRequest("dark", "de")).toEqual({ theme: "dark", language: "de", }); }); it("refuses what a tile's body is refused", () => { for (const theme of ["sepia", "Light", ""]) { expect(() => parseStyleRequest(theme, null)).toThrow(PayloadError); } for (const language of ["en", "de-DE", "DE", ""]) { expect(() => parseStyleRequest("light", language)).toThrow(PayloadError); } }); }); // Mirrored in the API, which bounds the tiles it stores by it. describe("LABEL_LANGUAGES", () => { it("is the basemap's own languages, bare codes, English left out", () => { expect(LABEL_LANGUAGES).toHaveLength(71); expect(new Set(LABEL_LANGUAGES).size).toBe(LABEL_LANGUAGES.length); expect([...LABEL_LANGUAGES].sort()).toEqual([...LABEL_LANGUAGES]); expect(LABEL_LANGUAGES.every((code) => /^[a-z]{2,3}$/.test(code))).toBe( true, ); expect(LABEL_LANGUAGES).not.toContain("en"); }); });