rwsdk
Version:
Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime
116 lines (115 loc) • 4.05 kB
JavaScript
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { setSyncedStateClientForTesting, } from "../client";
import { createSyncedStateHook, } from "../useSyncedState";
const createStateHarness = () => {
let currentState;
const cleanups = [];
const useStateImpl = ((initialValue) => {
const resolved = typeof initialValue === "function"
? initialValue()
: initialValue;
currentState = resolved;
const setState = (next) => {
currentState =
typeof next === "function"
? next(currentState)
: next;
};
return [currentState, setState];
});
const useEffectImpl = (callback) => {
const cleanup = callback();
if (typeof cleanup === "function") {
cleanups.push(cleanup);
}
};
const useRefImpl = ((value) => ({
current: value,
}));
const useCallbackImpl = (fn) => fn;
const deps = {
useState: useStateImpl,
useEffect: useEffectImpl,
useRef: useRefImpl,
useCallback: useCallbackImpl,
};
return {
deps,
getState: () => currentState,
runCleanups: () => cleanups.forEach((fn) => fn()),
};
};
describe("createSyncedStateHook", () => {
const subscribeHandlers = new Map();
const client = {
async getState() {
return 5;
},
async setState(_value, _key) { },
async subscribe(key, handler) {
subscribeHandlers.set(key, handler);
},
async unsubscribe(key) {
subscribeHandlers.delete(key);
},
};
const resetClient = () => {
client.getState = async () => 5;
client.setState = async (_value, _key) => { };
client.subscribe = async (key, handler) => {
subscribeHandlers.set(key, handler);
};
client.unsubscribe = async (key) => {
subscribeHandlers.delete(key);
};
};
beforeEach(() => {
resetClient();
setSyncedStateClientForTesting(client);
subscribeHandlers.clear();
});
afterEach(() => {
setSyncedStateClientForTesting(null);
});
it("loads remote state and updates local value", async () => {
const harness = createStateHarness();
const useSyncedState = createSyncedStateHook({ hooks: harness.deps });
const [value] = useSyncedState(0, "counter");
expect(value).toBe(0);
await Promise.resolve();
expect(harness.getState()).toBe(5);
});
it("sends updates through the client and applies optimistic value", async () => {
const harness = createStateHarness();
const setCalls = [];
client.setState = async (value, key) => {
setCalls.push({ key, value });
};
const useSyncedState = createSyncedStateHook({ hooks: harness.deps });
const [, setSyncValue] = useSyncedState(0, "counter");
setSyncValue(9);
expect(harness.getState()).toBe(9);
expect(setCalls).toEqual([{ key: "counter", value: 9 }]);
});
it("applies remote updates from the subscription handler", async () => {
const harness = createStateHarness();
const useSyncedState = createSyncedStateHook({ hooks: harness.deps });
useSyncedState(0, "counter");
await Promise.resolve();
const handler = subscribeHandlers.get("counter");
handler?.(7);
expect(harness.getState()).toBe(7);
});
it("unsubscribes during cleanup", () => {
const harness = createStateHarness();
const unsubscribed = [];
client.unsubscribe = async (key) => {
unsubscribed.push({ key });
subscribeHandlers.delete(key);
};
const useSyncedState = createSyncedStateHook({ hooks: harness.deps });
useSyncedState(0, "counter");
harness.runCleanups();
expect(unsubscribed).toEqual([{ key: "counter" }]);
});
});