Files
MeshChatX/tests/frontend/TileCache.test.js

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();
});
});
});