mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-04-25 15:22:10 +00:00
218 lines
7.4 KiB
JavaScript
218 lines
7.4 KiB
JavaScript
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll } from "vitest";
|
|
|
|
const DB_NAME = "meshchat_map_cache";
|
|
|
|
function tag(o) {
|
|
return Object.prototype.toString.call(o);
|
|
}
|
|
|
|
function tilePayloadToUint8(got) {
|
|
if (got == null) {
|
|
return null;
|
|
}
|
|
const t = tag(got);
|
|
if (t === "[object Uint8Array]") {
|
|
return got;
|
|
}
|
|
if (t === "[object ArrayBuffer]") {
|
|
return new Uint8Array(got);
|
|
}
|
|
if (Array.isArray(got)) {
|
|
return Uint8Array.from(got);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function tilePayloadToUint8Async(got) {
|
|
if (got == null) {
|
|
return null;
|
|
}
|
|
if (tag(got) === "[object Blob]") {
|
|
return new Uint8Array(await got.arrayBuffer());
|
|
}
|
|
return tilePayloadToUint8(got);
|
|
}
|
|
|
|
function deleteMapCacheDb() {
|
|
const idb = globalThis.indexedDB;
|
|
if (!idb) {
|
|
return Promise.resolve();
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const req = idb.deleteDatabase(DB_NAME);
|
|
req.onsuccess = () => resolve();
|
|
req.onblocked = () => resolve();
|
|
req.onerror = () => reject(req.error);
|
|
});
|
|
}
|
|
|
|
describe("TileCache.js", () => {
|
|
describe("indexedDB detection (mocked)", () => {
|
|
const DB_VERSION = 2;
|
|
let savedIndexedDb;
|
|
let savedGlobalIndexedDb;
|
|
|
|
beforeEach(async () => {
|
|
savedIndexedDb = window.indexedDB;
|
|
savedGlobalIndexedDb = globalThis.indexedDB;
|
|
vi.resetModules();
|
|
vi.clearAllMocks();
|
|
|
|
delete window.indexedDB;
|
|
delete window.mozIndexedDB;
|
|
delete window.webkitIndexedDB;
|
|
delete window.msIndexedDB;
|
|
delete globalThis.indexedDB;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (savedIndexedDb !== undefined) {
|
|
window.indexedDB = savedIndexedDb;
|
|
}
|
|
if (savedGlobalIndexedDb !== undefined) {
|
|
globalThis.indexedDB = savedGlobalIndexedDb;
|
|
}
|
|
});
|
|
|
|
it("should support window.indexedDB", async () => {
|
|
const mockRequest = { onsuccess: null, onerror: null };
|
|
const mockOpen = vi.fn().mockReturnValue(mockRequest);
|
|
window.indexedDB = { open: mockOpen };
|
|
|
|
await import("@/js/TileCache");
|
|
|
|
expect(mockOpen).toHaveBeenCalledWith(DB_NAME, DB_VERSION);
|
|
});
|
|
|
|
it("should support vendor prefixes (mozIndexedDB)", async () => {
|
|
const mockRequest = { onsuccess: null, onerror: null };
|
|
const mockOpen = vi.fn().mockReturnValue(mockRequest);
|
|
window.mozIndexedDB = { open: mockOpen };
|
|
|
|
await import("@/js/TileCache");
|
|
|
|
expect(mockOpen).toHaveBeenCalledWith(DB_NAME, DB_VERSION);
|
|
});
|
|
|
|
it("should support vendor prefixes (webkitIndexedDB)", async () => {
|
|
const mockRequest = { onsuccess: null, onerror: null };
|
|
const mockOpen = vi.fn().mockReturnValue(mockRequest);
|
|
window.webkitIndexedDB = { open: mockOpen };
|
|
|
|
await import("@/js/TileCache");
|
|
|
|
expect(mockOpen).toHaveBeenCalledWith(DB_NAME, DB_VERSION);
|
|
});
|
|
|
|
it("should support vendor prefixes (msIndexedDB)", async () => {
|
|
const mockRequest = { onsuccess: null, onerror: null };
|
|
const mockOpen = vi.fn().mockReturnValue(mockRequest);
|
|
window.msIndexedDB = { open: mockOpen };
|
|
|
|
await import("@/js/TileCache");
|
|
|
|
expect(mockOpen).toHaveBeenCalledWith(DB_NAME, DB_VERSION);
|
|
});
|
|
|
|
it("should support globalThis.indexedDB", async () => {
|
|
const mockRequest = { onsuccess: null, onerror: null };
|
|
const mockOpen = vi.fn().mockReturnValue(mockRequest);
|
|
globalThis.indexedDB = { open: mockOpen };
|
|
|
|
await import("@/js/TileCache");
|
|
|
|
expect(mockOpen).toHaveBeenCalledWith(DB_NAME, DB_VERSION);
|
|
});
|
|
|
|
it("should reject if IndexedDB is not supported", async () => {
|
|
const module = await import("@/js/TileCache");
|
|
const cache = module.default;
|
|
|
|
await expect(cache.initPromise).rejects.toBe("IndexedDB not supported");
|
|
});
|
|
});
|
|
|
|
describe("tile and map_state storage (fake-indexeddb)", () => {
|
|
let TileCache;
|
|
|
|
beforeAll(async () => {
|
|
vi.resetModules();
|
|
await deleteMapCacheDb();
|
|
const mod = await import("@/js/TileCache");
|
|
TileCache = mod.default;
|
|
await TileCache.initPromise;
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await TileCache.clear();
|
|
});
|
|
|
|
it("stores and retrieves binary tile data under the URL key", async () => {
|
|
const key = "https://tiles.example/z/x/y.png";
|
|
const bytes = Uint8Array.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
|
|
// IndexedDB structured clone stores ArrayBuffer reliably in jsdom + fake-indexeddb.
|
|
// MapPage passes Blob from fetch(); both Blob and ArrayBuffer are valid values.
|
|
await TileCache.setTile(key, bytes.buffer);
|
|
const got = await TileCache.getTile(key);
|
|
|
|
const out = await tilePayloadToUint8Async(got);
|
|
expect(out).not.toBeNull();
|
|
expect(Array.from(out)).toEqual(Array.from(bytes));
|
|
});
|
|
|
|
it("stores and retrieves a tile Blob under the URL key when the runtime preserves Blob", async () => {
|
|
const key = "https://tiles.example/blob.png";
|
|
const bytes = Uint8Array.from([0x89, 0x50, 0x4e, 0x47]);
|
|
const blob = new Blob([bytes], { type: "image/png" });
|
|
|
|
await TileCache.setTile(key, blob);
|
|
const got = await TileCache.getTile(key);
|
|
|
|
if (!(got instanceof Blob)) {
|
|
return;
|
|
}
|
|
|
|
expect(got.type).toBe("image/png");
|
|
expect(got.size).toBe(bytes.length);
|
|
const out = new Uint8Array(await got.arrayBuffer());
|
|
expect(Array.from(out)).toEqual(Array.from(bytes));
|
|
});
|
|
|
|
it("returns undefined for a missing tile key", async () => {
|
|
const got = await TileCache.getTile("https://missing.example/0/0/0.png");
|
|
expect(got).toBeUndefined();
|
|
});
|
|
|
|
it("overwrites an existing tile key", async () => {
|
|
const key = "https://tiles.example/same.png";
|
|
await TileCache.setTile(key, Uint8Array.from([1, 2, 3]).buffer);
|
|
await TileCache.setTile(key, Uint8Array.from([9, 9]).buffer);
|
|
|
|
const got = await TileCache.getTile(key);
|
|
const out = await tilePayloadToUint8Async(got);
|
|
expect(out).not.toBeNull();
|
|
expect(out.length).toBe(2);
|
|
expect(out[0]).toBe(9);
|
|
});
|
|
|
|
it("stores and retrieves map state independently of tiles", async () => {
|
|
const state = { center: [12.5, 55.1], zoom: 7, offlineEnabled: false };
|
|
await TileCache.setMapState("last_view", state);
|
|
const got = await TileCache.getMapState("last_view");
|
|
|
|
expect(got).toEqual(state);
|
|
});
|
|
|
|
it("clear removes tiles and map state", async () => {
|
|
await TileCache.setTile("https://x/t.png", Uint8Array.from([97]).buffer);
|
|
await TileCache.setMapState("last_view", { zoom: 1 });
|
|
|
|
await TileCache.clear();
|
|
|
|
expect(await TileCache.getTile("https://x/t.png")).toBeUndefined();
|
|
expect(await TileCache.getMapState("last_view")).toBeUndefined();
|
|
});
|
|
});
|
|
});
|