From fb6a4700cd434d783260a871e83d9533da5dc18f Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Wed, 26 Feb 2025 14:34:13 -0800 Subject: [PATCH] fix storage tests --- .../packages/common-charm/src/storage.ts | 7 +- .../common-charm/test/storage.test.ts | 363 ++++++------------ 2 files changed, 126 insertions(+), 244 deletions(-) diff --git a/typescript/packages/common-charm/src/storage.ts b/typescript/packages/common-charm/src/storage.ts index 7a1d61167..9c00bec91 100644 --- a/typescript/packages/common-charm/src/storage.ts +++ b/typescript/packages/common-charm/src/storage.ts @@ -233,7 +233,12 @@ class StorageImpl implements Storage { let provider = this.storageProviders.get(space); if (!provider) { - const type = (import.meta as any).env?.VITE_STORAGE_TYPE ?? "remote"; + // Default to "remote", but let either custom URL (used in tests) or + // environment variable override this. + const type = + this.remoteStorageUrl?.protocol === "memory:" + ? "memory" + : ((import.meta as any).env?.VITE_STORAGE_TYPE ?? "remote"); if (type === "remote") { if (!this.remoteStorageUrl) throw new Error("No remote storage URL set"); diff --git a/typescript/packages/common-charm/test/storage.test.ts b/typescript/packages/common-charm/test/storage.test.ts index bc71f47d6..a42f139ea 100644 --- a/typescript/packages/common-charm/test/storage.test.ts +++ b/typescript/packages/common-charm/test/storage.test.ts @@ -1,276 +1,153 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; -import { createStorage, Storage } from "../src/storage.js"; +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { storage } from "../src/storage.js"; import { StorageProvider } from "../src/storage/base.js"; import { InMemoryStorageProvider } from "../src/storage/memory.js"; -import { LocalStorageProvider } from "../src/storage/localstorage.js"; -import { getDoc, DocImpl, createRef } from "@commontools/runner"; +import { getDoc, DocImpl, createRef, getSpace } from "@commontools/runner"; -// Create a mock window object -const createMockWindow = () => { - let listeners = new Set<(event: any) => void>(); - - const mockWindow: any = { - localStorage: { - getItem: vi.fn((key: string) => mockWindow.localStorage[key] || null), - setItem: vi.fn((key: string, value: string) => { - const oldValue = mockWindow.localStorage[key]; - mockWindow.localStorage[key] = value; - mockWindow.dispatchEvent( - new StorageEvent("storage", { - key, - oldValue, - newValue: value, - storageArea: mockWindow.localStorage, - url: mockWindow.location.href, - }), - ); - }), - removeItem: vi.fn((key: string) => { - const oldValue = mockWindow.localStorage[key]; - delete mockWindow.localStorage[key]; - mockWindow.dispatchEvent( - new StorageEvent("storage", { - key, - oldValue, - newValue: null, - storageArea: mockWindow.localStorage, - url: mockWindow.location.href, - }), - ); - }), - clear: vi.fn(() => { - mockWindow.localStorage = {}; - mockWindow.dispatchEvent( - new StorageEvent("storage", { - key: null, - oldValue: null, - newValue: null, - storageArea: mockWindow.localStorage, - url: mockWindow.location.href, - }), - ); - }), - }, - addEventListener: vi.fn((event: string, callback: (event: any) => void) => { - if (event === "storage") listeners.add(callback); - }), - removeEventListener: vi.fn((event: string, callback: (event: any) => void) => { - if (event === "storage") listeners.delete(callback); - }), - dispatchEvent: vi.fn((event: any) => { - if (event.type === "storage") { - listeners.forEach((callback) => callback(event)); - } - }), - location: { - href: "http://localhost", - }, - }; - - return mockWindow; -}; - -// Create the mock window -const mockWindow = createMockWindow(); - -// Mock the global window object -vi.stubGlobal("window", mockWindow); -vi.stubGlobal("localStorage", mockWindow.localStorage); - -// Mock StorageEvent if it doesn't exist -if (typeof StorageEvent === "undefined") { - (global as any).StorageEvent = class StorageEvent { - type: string; - key: string | null; - oldValue: string | null; - newValue: string | null; - url: string; - storageArea: any; - - constructor(type: string, eventInitDict?: any) { - this.type = type; - this.key = eventInitDict?.key ?? null; - this.oldValue = eventInitDict?.oldValue ?? null; - this.newValue = eventInitDict?.newValue ?? null; - this.url = eventInitDict?.url ?? ""; - this.storageArea = eventInitDict?.storageArea ?? null; - } - }; -} +storage.setRemoteStorage(new URL("memory://")); describe("Storage", () => { - const storageTypes = ["memory", "local"] as const; - - storageTypes.forEach((storageType) => { - describe(storageType, () => { - let storage: Storage; - let storage2: StorageProvider; - let testCell: DocImpl; - - beforeEach(() => { - storage = createStorage({ type: storageType as "memory" | "local" }); - if (storageType === "memory") { - storage2 = new InMemoryStorageProvider(); - } else if (storageType === "local") { - console.log("local", LocalStorageProvider); - storage2 = new LocalStorageProvider(); - } else { - throw new Error("Invalid storage type: " + storageType); - } - testCell = getDoc(); - testCell.generateEntityId(); - }); - - afterEach(async () => { - await storage?.destroy(); - await storage2?.destroy(); - }); - - describe("persistCell", () => { - it("should persist a cell", async () => { - const testValue = { data: "test" }; - testCell.send(testValue); - - await storage.syncCell(testCell); - - await storage2.sync(testCell.entityId!); - const value = storage2.get(testCell.entityId!); - expect(value?.value).toEqual(testValue); - }); - - it("should persist a cells and referenced cell references within it", async () => { - const refCell = getDoc("hello"); - refCell.generateEntityId(); + let storage2: StorageProvider; + let testCell: DocImpl; - const testValue = { - data: "test", - ref: { cell: refCell, path: [] }, - }; - testCell.send(testValue); + beforeEach(() => { + storage2 = new InMemoryStorageProvider("test"); + testCell = getDoc(); + testCell.generateEntityId(undefined, getSpace("test")); + }); - console.log("syncing testCell"); - await storage.syncCell(testCell); - console.log("synced testCell"); + afterEach(async () => { + await storage?.cancelAll(); + await storage2?.destroy(); + }); - await storage2.sync(refCell.entityId!); - const value = storage2.get(refCell.entityId!); - expect(value?.value).toEqual("hello"); - }); + describe("persistCell", () => { + it("should persist a cell", async () => { + const testValue = { data: "test" }; + testCell.send(testValue); - it("should persist a cells and referenced cells within it", async () => { - const refCell = getDoc("hello"); - refCell.generateEntityId(); + await storage.syncCell(testCell); - const testValue = { - data: "test", - otherCell: refCell, - }; - testCell.send(testValue); + await storage2.sync(testCell.entityId!); + const value = storage2.get(testCell.entityId!); + expect(value?.value).toEqual(testValue); + }); - await storage.syncCell(testCell); + it("should persist a cells and referenced cell references within it", async () => { + const refCell = getDoc("hello"); + refCell.generateEntityId(undefined, getSpace("test")); - await storage2.sync(refCell.entityId!); - const value = storage2.get(refCell.entityId!); - expect(value?.value).toEqual("hello"); - }); + const testValue = { + data: "test", + ref: { cell: refCell, path: [] }, + }; + testCell.send(testValue); - it("should generate causal IDs for cells that don't have them yet", async () => { - const testValue = { - data: "test", - ref: { cell: getDoc("hello"), path: [] }, - }; - testCell.send(testValue); + console.log("syncing testCell"); + await storage.syncCell(testCell); + console.log("synced testCell"); - await storage.syncCell(testCell); + await storage2.sync(refCell.entityId!); + const value = storage2.get(refCell.entityId!); + expect(value?.value).toEqual("hello"); + }); - const refId = createRef( - { value: "hello" }, - { - cell: testCell.entityId?.toJSON?.(), - path: ["ref"], - }, - ); - await storage2.sync(refId); - const value = storage2.get(refId); - expect(value?.value).toEqual("hello"); - }); - }); + it("should persist a cells and referenced cells within it", async () => { + const refCell = getDoc("hello"); + refCell.generateEntityId(undefined, getSpace("test")); - describe("cell updates", () => { - it("should persist cell updates", async () => { - await storage.syncCell(testCell); + const testValue = { + data: "test", + otherCell: refCell, + }; + testCell.send(testValue); - testCell.send("value 1"); - testCell.send("value 2"); + await storage.syncCell(testCell); - await storage.synced(); + await storage2.sync(refCell.entityId!); + const value = storage2.get(refCell.entityId!); + expect(value?.value).toEqual("hello"); + }); - await storage2.sync(testCell.entityId!); - const value = storage2.get(testCell.entityId!); - expect(value?.value).toBe("value 2"); - }); - }); + it("should generate causal IDs for cells that don't have them yet", async () => { + const testValue = { + data: "test", + ref: { cell: getDoc("hello"), path: [] }, + }; + testCell.send(testValue); + + await storage.syncCell(testCell); + + const refId = createRef( + { value: "hello" }, + { + cell: testCell.entityId?.toJSON?.(), + path: ["ref"], + }, + ); + await storage2.sync(refId); + const value = storage2.get(refId); + expect(value?.value).toEqual("hello"); + }); + }); - describe("syncCell", () => { - it("should only load a cell once", async () => { - const cell1 = await storage.syncCell(testCell); - expect(cell1).toBe(testCell); + describe("cell updates", () => { + it("should persist cell updates", async () => { + await storage.syncCell(testCell); - // Even when passing in a new cell with the same entityId, it should be - // the same cell. - const cell2 = getDoc(); - cell2.entityId = testCell.entityId; - const cell3 = await storage.syncCell(cell2); - expect(cell3).toBe(cell1); - }); + testCell.send("value 1"); + testCell.send("value 2"); - it("should wait for a cell to appear", async () => { - let synced = false; - storage2.sync(testCell.entityId!, true).then(() => (synced = true)); - expect(synced).toBe(false); + await storage.synced(); - testCell.send("test"); - await storage.syncCell(testCell); - expect(synced).toBe(true); - }); + await storage2.sync(testCell.entityId!); + const value = storage2.get(testCell.entityId!); + expect(value?.value).toBe("value 2"); + }); + }); - it("should wait for a undefined cell to appear", async () => { - let synced = false; - storage2.sync(testCell.entityId!, true).then(() => (synced = true)); - expect(synced).toBe(false); + describe("syncCell", () => { + it("should only load a cell once", async () => { + const cell1 = await storage.syncCell(testCell); + expect(cell1).toBe(testCell); + + // Even when passing in a new cell with the same entityId, it should be + // the same cell. + const cell2 = getDoc(); + cell2.entityId = testCell.entityId; + const cell3 = await storage.syncCell(cell2); + expect(cell3).toBe(cell1); + }); - await storage.syncCell(testCell); - expect(synced).toBe(true); - }); - }); + it("should wait for a cell to appear", async () => { + let synced = false; + storage2.sync(testCell.entityId!, true).then(() => (synced = true)); + expect(synced).toBe(false); - describe("ephemeral cells", () => { - it("should not be loaded from storage", async () => { - const ephemeralCell = getDoc("transient", "ephemeral"); - ephemeralCell.ephemeral = true; - await storage.syncCell(ephemeralCell); + testCell.send("test"); + await storage.syncCell(testCell); + expect(synced).toBe(true); + }); - await storage2.sync(ephemeralCell.entityId!); - const value = storage2.get(ephemeralCell.entityId!); - expect(value).toBeUndefined(); - }); - }); + it("should wait for a undefined cell to appear", async () => { + let synced = false; + storage2.sync(testCell.entityId!, true).then(() => (synced = true)); + expect(synced).toBe(false); - describe("createStorage", () => { - it("should create memory storage", () => { - const memoryStorage = createStorage({ type: "memory" }); - expect(memoryStorage).toBeDefined(); - }); + await storage.syncCell(testCell); + expect(synced).toBe(true); + }); + }); - it("should create local storage", () => { - const localStorage = createStorage({ type: "local" }); - expect(localStorage).toBeDefined(); - }); + describe("ephemeral cells", () => { + it("should not be loaded from storage", async () => { + const ephemeralCell = getDoc("transient", "ephemeral", getSpace("test")); + ephemeralCell.ephemeral = true; + await storage.syncCell(ephemeralCell); - it("should throw an error for invalid storage type", () => { - expect(() => createStorage("invalid" as any)).toThrow("Invalid storage type"); - }); - }); + await storage2.sync(ephemeralCell.entityId!); + const value = storage2.get(ephemeralCell.entityId!); + expect(value).toBeUndefined(); }); }); });