Files
wehub-resource-sync c48612c494
CI / E2E Tests (push) Has been cancelled
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
Docs Build / Build docs site (push) Has been cancelled
Publish @openmaic packages / Build, validate & publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:23 +08:00

156 lines
4.5 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
editLockKey,
isEditLockHeldByOther,
LOCK_HEARTBEAT_MS,
LOCK_STALE_MS,
readEditLock,
refreshEditLock,
releaseEditLock,
tryAcquireEditLock,
} from '@/lib/edit/edit-mode-lock';
class MemoryStorage {
private store = new Map<string, string>();
get length() {
return this.store.size;
}
getItem(k: string) {
return this.store.has(k) ? (this.store.get(k) as string) : null;
}
setItem(k: string, v: string) {
this.store.set(k, v);
}
removeItem(k: string) {
this.store.delete(k);
}
clear() {
this.store.clear();
}
key(i: number) {
return Array.from(this.store.keys())[i] ?? null;
}
}
let original: typeof globalThis.localStorage | undefined;
beforeEach(() => {
original = (globalThis as { localStorage?: typeof globalThis.localStorage }).localStorage;
Object.defineProperty(globalThis, 'localStorage', {
value: new MemoryStorage(),
configurable: true,
writable: true,
});
});
afterEach(() => {
Object.defineProperty(globalThis, 'localStorage', {
value: original,
configurable: true,
writable: true,
});
});
describe('editLockKey', () => {
it('is scoped per course id', () => {
expect(editLockKey('course-A')).not.toBe(editLockKey('course-B'));
});
});
describe('tryAcquireEditLock', () => {
it('grants the lock when nothing is held', () => {
expect(tryAcquireEditLock('c1', 'tab-A')).toBe(true);
expect(readEditLock('c1')?.tabId).toBe('tab-A');
});
it('grants the lock when our own tab already holds it', () => {
tryAcquireEditLock('c1', 'tab-A', 1000);
expect(tryAcquireEditLock('c1', 'tab-A', 2000)).toBe(true);
expect(readEditLock('c1')?.timestamp).toBe(2000);
});
it('refuses when another tab holds a fresh lock', () => {
tryAcquireEditLock('c1', 'tab-A', 1000);
expect(tryAcquireEditLock('c1', 'tab-B', 1000 + LOCK_HEARTBEAT_MS)).toBe(false);
// Original owner unchanged.
expect(readEditLock('c1')?.tabId).toBe('tab-A');
});
it('steals a stale lock from a crashed tab past LOCK_STALE_MS', () => {
tryAcquireEditLock('c1', 'tab-A', 1000);
expect(tryAcquireEditLock('c1', 'tab-B', 1000 + LOCK_STALE_MS + 1)).toBe(true);
expect(readEditLock('c1')?.tabId).toBe('tab-B');
});
it('does not bleed across courses', () => {
tryAcquireEditLock('c1', 'tab-A');
expect(tryAcquireEditLock('c2', 'tab-B')).toBe(true);
});
});
describe('isEditLockHeldByOther', () => {
it('returns false when nobody holds the lock', () => {
expect(isEditLockHeldByOther('c1', 'tab-A')).toBe(false);
});
it('returns false when our tab holds the lock', () => {
tryAcquireEditLock('c1', 'tab-A');
expect(isEditLockHeldByOther('c1', 'tab-A')).toBe(false);
});
it('returns true when another tab holds a fresh lock', () => {
tryAcquireEditLock('c1', 'tab-A', 1000);
expect(isEditLockHeldByOther('c1', 'tab-B', 1000 + LOCK_HEARTBEAT_MS)).toBe(true);
});
it("returns false when the other tab's lock is stale", () => {
tryAcquireEditLock('c1', 'tab-A', 1000);
expect(isEditLockHeldByOther('c1', 'tab-B', 1000 + LOCK_STALE_MS + 1)).toBe(false);
});
});
describe('refreshEditLock', () => {
it('updates the timestamp for our tab', () => {
tryAcquireEditLock('c1', 'tab-A', 1000);
refreshEditLock('c1', 'tab-A', 5000);
expect(readEditLock('c1')?.timestamp).toBe(5000);
});
it("does not overwrite another tab's lock", () => {
tryAcquireEditLock('c1', 'tab-A', 1000);
refreshEditLock('c1', 'tab-B', 2000);
expect(readEditLock('c1')?.tabId).toBe('tab-A');
expect(readEditLock('c1')?.timestamp).toBe(1000);
});
});
describe('releaseEditLock', () => {
it('removes the lock when we own it', () => {
tryAcquireEditLock('c1', 'tab-A');
releaseEditLock('c1', 'tab-A');
expect(readEditLock('c1')).toBeNull();
});
it('is a no-op when another tab owns the lock', () => {
tryAcquireEditLock('c1', 'tab-A');
releaseEditLock('c1', 'tab-B');
expect(readEditLock('c1')?.tabId).toBe('tab-A');
});
it('is a no-op when no lock exists', () => {
expect(() => releaseEditLock('c1', 'tab-A')).not.toThrow();
});
});
describe('graceful degradation', () => {
it('returns null when stored JSON is corrupted', () => {
localStorage.setItem(editLockKey('c1'), '{not json');
expect(readEditLock('c1')).toBeNull();
});
it('returns null when stored shape is wrong', () => {
localStorage.setItem(editLockKey('c1'), JSON.stringify({ wrong: 'shape' }));
expect(readEditLock('c1')).toBeNull();
});
});