TypeScript 87.7%
CSS 12.3%
1/**2 * Author: Simon-Pierre Boucher3 * Contact: contact@spboucher.ai4 * Project: Groupe Ka / Ka Maps5 */67import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";8import {9 BoundsQueryScheduler,10 stableStringify,11} from "../src/services/boundsQuery.js";12import type {13 BoundsQuery,14 BoundsQueryResult,15 KaDataAdapter,16 MapProperty,17} from "../src/types/index.js";1819const BOX = { west: -71.4, south: 46.7, east: -71.1, north: 46.9 };2021function prop(id: string): MapProperty {22 return {23 id,24 appSource: "lou-ka",25 latitude: 46.8,26 longitude: -71.2,27 kind: "listing",28 price: 1000,29 };30}3132function makeAdapter(33 impl: (q: BoundsQuery) => Promise<BoundsQueryResult>,34): KaDataAdapter {35 return { id: "test", appSource: "lou-ka", fetchInBounds: impl };36}3738beforeEach(() => vi.useFakeTimers());39afterEach(() => vi.useRealTimers());4041describe("BoundsQueryScheduler", () => {42 it("debounces bursts into one fetch", async () => {43 const calls: BoundsQuery[] = [];44 const s = new BoundsQueryScheduler(45 makeAdapter(async (q) => {46 calls.push(q);47 return { properties: [prop("a")] };48 }),49 { debounceMs: 100 },50 );51 s.request({ bbox: BOX, zoom: 12 });52 s.request({ bbox: BOX, zoom: 12.5 });53 s.request({ bbox: BOX, zoom: 13 });54 await vi.advanceTimersByTimeAsync(150);55 expect(calls).toHaveLength(1);56 expect(calls[0]!.zoom).toBe(13);57 s.destroy();58 });5960 it("aborts the stale request and never delivers old results late", async () => {61 const results: string[] = [];62 let call = 0;63 const s = new BoundsQueryScheduler(64 makeAdapter((q) => {65 const mine = ++call;66 return new Promise((resolve, reject) => {67 q.signal.addEventListener("abort", () =>68 reject(new DOMException("aborted", "AbortError")),69 );70 // First call resolves slowly, second quickly.71 setTimeout(72 () => resolve({ properties: [prop(`r${mine}`)] }),73 mine === 1 ? 500 : 10,74 );75 });76 }),77 { debounceMs: 0, cacheTtlMs: 0 },78 );79 s.onResult((r) => results.push(r.properties[0]!.id));8081 s.requestNow({ bbox: BOX, zoom: 10 });82 await vi.advanceTimersByTimeAsync(5);83 s.requestNow({ bbox: { ...BOX, north: 47.0 }, zoom: 11 });84 await vi.advanceTimersByTimeAsync(600);8586 expect(results).toEqual(["r2"]);87 s.destroy();88 });8990 it("serves identical queries from cache", async () => {91 let fetches = 0;92 const s = new BoundsQueryScheduler(93 makeAdapter(async () => {94 fetches++;95 return { properties: [prop("a")], totalCount: 1 };96 }),97 { debounceMs: 0 },98 );99 const seen: number[] = [];100 s.onResult((r) => seen.push(r.totalCount ?? 0));101102 s.requestNow({ bbox: BOX, zoom: 12, filters: { city: "Québec" } });103 await vi.advanceTimersByTimeAsync(10);104 s.requestNow({ bbox: BOX, zoom: 12, filters: { city: "Québec" } });105 await vi.advanceTimersByTimeAsync(10);106107 expect(fetches).toBe(1);108 expect(seen).toHaveLength(2);109 s.destroy();110 });111112 it("reports errors only for the newest request", async () => {113 const errors: unknown[] = [];114 const s = new BoundsQueryScheduler(115 makeAdapter(async () => {116 throw new Error("boom");117 }),118 { debounceMs: 0 },119 );120 s.onError((e) => errors.push(e));121 s.requestNow({ bbox: BOX, zoom: 12 });122 await vi.advanceTimersByTimeAsync(10);123 expect(errors).toHaveLength(1);124 s.destroy();125 });126});127128describe("stableStringify", () => {129 it("is key-order independent", () => {130 expect(stableStringify({ b: 1, a: { d: 2, c: 3 } })).toBe(131 stableStringify({ a: { c: 3, d: 2 }, b: 1 }),132 );133 });134});135