feat: map shapes, image persistence, grab tool, layer rename/delete, i18n, page metadata

This commit is contained in:
AlacrisDevs
2026-02-08 23:11:09 +02:00
parent 75a2aefadb
commit f2384bceb8
125 changed files with 22605 additions and 3902 deletions

View File

@@ -0,0 +1,146 @@
import { describe, it, expect, vi } from 'vitest';
import {
getLockInfo,
acquireLock,
heartbeatLock,
releaseLock,
startHeartbeat,
} from './document-locks';
// ── Supabase mock builder ────────────────────────────────────────────────────
function mockChain(resolvedValue: { data: any; error: any }) {
const chain: any = {};
const methods = ['from', 'select', 'insert', 'update', 'delete', 'eq', 'gt', 'lt', 'single'];
for (const m of methods) {
chain[m] = vi.fn(() => chain);
}
chain.single = vi.fn(() => Promise.resolve(resolvedValue));
chain.then = (resolve: any) => resolve(resolvedValue);
return chain;
}
function mockSupabase(resolvedValue: { data: any; error: any }) {
const chain = mockChain(resolvedValue);
return { from: vi.fn(() => chain), _chain: chain } as any;
}
// ── getLockInfo ──────────────────────────────────────────────────────────────
describe('getLockInfo', () => {
it('returns unlocked when no lock exists', async () => {
const sb = mockSupabase({ data: null, error: null });
const info = await getLockInfo(sb, 'doc1', 'user1');
expect(info).toEqual({
isLocked: false,
lockedBy: null,
lockedByName: null,
isOwnLock: false,
});
});
it('returns locked with own lock', async () => {
// First call returns lock, second returns profile
const lockData = { id: 'l1', document_id: 'doc1', user_id: 'user1', locked_at: new Date().toISOString(), last_heartbeat: new Date().toISOString() };
const profileData = { full_name: 'Alice', email: 'alice@test.com' };
const chain: any = {};
const methods = ['from', 'select', 'eq', 'gt', 'single'];
for (const m of methods) {
chain[m] = vi.fn(() => chain);
}
let callCount = 0;
chain.single = vi.fn(() => {
callCount++;
if (callCount === 1) return Promise.resolve({ data: lockData, error: null });
return Promise.resolve({ data: profileData, error: null });
});
const sb = { from: vi.fn(() => chain) } as any;
const info = await getLockInfo(sb, 'doc1', 'user1');
expect(info.isLocked).toBe(true);
expect(info.isOwnLock).toBe(true);
expect(info.lockedByName).toBe('Alice');
});
it('returns locked with other user lock', async () => {
const lockData = { id: 'l1', document_id: 'doc1', user_id: 'user2', locked_at: new Date().toISOString(), last_heartbeat: new Date().toISOString() };
const profileData = { full_name: 'Bob', email: 'bob@test.com' };
const chain: any = {};
const methods = ['from', 'select', 'eq', 'gt', 'single'];
for (const m of methods) {
chain[m] = vi.fn(() => chain);
}
let callCount = 0;
chain.single = vi.fn(() => {
callCount++;
if (callCount === 1) return Promise.resolve({ data: lockData, error: null });
return Promise.resolve({ data: profileData, error: null });
});
const sb = { from: vi.fn(() => chain) } as any;
const info = await getLockInfo(sb, 'doc1', 'user1');
expect(info.isLocked).toBe(true);
expect(info.isOwnLock).toBe(false);
expect(info.lockedByName).toBe('Bob');
});
});
// ── acquireLock ──────────────────────────────────────────────────────────────
describe('acquireLock', () => {
it('returns true when lock acquired', async () => {
const sb = mockSupabase({ data: null, error: null });
expect(await acquireLock(sb, 'doc1', 'user1')).toBe(true);
});
it('returns false on unique constraint violation', async () => {
const sb = mockSupabase({ data: null, error: { code: '23505', message: 'unique' } });
expect(await acquireLock(sb, 'doc1', 'user1')).toBe(false);
});
it('returns false on other errors', async () => {
const sb = mockSupabase({ data: null, error: { code: '42000', message: 'other' } });
expect(await acquireLock(sb, 'doc1', 'user1')).toBe(false);
});
});
// ── heartbeatLock ────────────────────────────────────────────────────────────
describe('heartbeatLock', () => {
it('returns true on success', async () => {
const sb = mockSupabase({ data: null, error: null });
expect(await heartbeatLock(sb, 'doc1', 'user1')).toBe(true);
});
it('returns false on error', async () => {
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
expect(await heartbeatLock(sb, 'doc1', 'user1')).toBe(false);
});
});
// ── releaseLock ──────────────────────────────────────────────────────────────
describe('releaseLock', () => {
it('releases without error', async () => {
const sb = mockSupabase({ data: null, error: null });
await expect(releaseLock(sb, 'doc1', 'user1')).resolves.toBeUndefined();
});
it('does not throw on error (just logs)', async () => {
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
await expect(releaseLock(sb, 'doc1', 'user1')).resolves.toBeUndefined();
});
});
// ── startHeartbeat ───────────────────────────────────────────────────────────
describe('startHeartbeat', () => {
it('returns a cleanup function', () => {
const sb = mockSupabase({ data: null, error: null });
const cleanup = startHeartbeat(sb, 'doc1', 'user1');
expect(typeof cleanup).toBe('function');
cleanup(); // should not throw
});
});