// jest manual mock: firebase/firestore // Records all calls so tests can assert path/payload/semantics. // Global __firestoreCalls reset per test (see setupTests.js). import { MOCK_DB, recordCall } from './_mock-db.js'; const ref = (path) => ({ __ref: true, path, id: path.split('/').pop() }); export function getFirestore() { return { __db: true }; } export function doc(db, path, extra) { const p = extra ? `${path}/${extra}` : path; return ref(p); } export function collection(db, path) { return ref(path); } export function query(refOrColl, ...constraints) { return { ref: refOrColl, constraints }; } export function where(field, op, value) { return { __type: 'where', field, op, value }; } export function orderBy(field, dir) { return { __type: 'orderBy', field, dir }; } export function limit(n) { return { __type: 'limit', n }; } // writes export async function setDoc(docRef, data, opts) { recordCall({ fn: 'setDoc', path: docRef.path, data: clone(data), opts: opts || null }); if (opts && opts.merge) { MOCK_DB.merge(docRef.path, clone(data)); } else { MOCK_DB.set(docRef.path, clone(data)); } return undefined; } export async function updateDoc(docRef, patch) { recordCall({ fn: 'updateDoc', path: docRef.path, data: clone(patch) }); MOCK_DB.merge(docRef.path, clone(patch)); return undefined; } export async function deleteDoc(docRef) { recordCall({ fn: 'deleteDoc', path: docRef.path }); MOCK_DB.delete(docRef.path); return undefined; } export async function addDoc(collRef, data) { const id = `auto_${MOCK_DB.nextId()}`; const path = `${collRef.path}/${id}`; recordCall({ fn: 'addDoc', path, data: clone(data) }); MOCK_DB.set(path, clone(data)); return { id, path }; } export function writeBatch(db) { const ops = []; return { set: (r, d) => ops.push({ op: 'set', path: r.path, data: clone(d) }), update: (r, d) => ops.push({ op: 'update', path: r.path, data: clone(d) }), delete: (r) => ops.push({ op: 'delete', path: r.path }), commit: async () => { ops.forEach(o => { recordCall({ fn: `batch.${o.op}`, path: o.path, data: o.data }); if (o.op === 'set') MOCK_DB.set(o.path, o.data); else if (o.op === 'update') MOCK_DB.merge(o.path, o.data); else if (o.op === 'delete') MOCK_DB.delete(o.path); }); }, }; } // reads (return from in-memory mock DB) export async function getDoc(docRef) { const data = MOCK_DB.get(docRef.path); return { exists: () => data !== null, id: docRef.id, data: () => data }; } export async function getDocs(collRefOrQuery) { const collPath = collRefOrQuery.ref ? collRefOrQuery.ref.path : collRefOrQuery.path; let docs = MOCK_DB.collection(collPath); const constraints = collRefOrQuery.constraints || []; docs = applyConstraints(docs, constraints); return { docs: docs.map(d => ({ id: d.id, data: () => d.data, ref: { path: `${collPath}/${d.id}` } })) }; } export async function getCountFromServer(collRefOrQuery) { const collPath = collRefOrQuery.ref ? collRefOrQuery.ref.path : collRefOrQuery.path; const docs = MOCK_DB.collection(collPath); return { data: () => ({ count: docs.length }) }; } // realtime — emit from mock DB, capture unsub export function onSnapshot(refOrQuery, onSuccess, onError) { const path = refOrQuery.path || (refOrQuery.ref && refOrQuery.ref.path); const constraints = refOrQuery.constraints || []; // fire immediately with current state const emit = () => { if (refOrQuery.__ref && refOrQuery.path && path.split('/').length % 2 === 0) { const data = MOCK_DB.get(path); onSuccess({ exists: () => data !== null, id: path.split('/').pop(), data: () => data, }); } else { let docs = MOCK_DB.collection(path); docs = applyConstraints(docs, constraints); onSuccess({ docs: docs.map(d => ({ id: d.id, data: () => d.data })) }); } }; emit(); // register for future changes on this path const unsub = MOCK_DB.subscribe(path, emit); return unsub; } // Apply Firestore-style query constraints (orderBy desc/asc, limit, where) // to mock docs. Mirrors real SDK semantics for contract tests. Offset handled // by adapter (firebase.js slices) — mock never sees it. function applyConstraints(docs, constraints) { let out = [...docs]; for (const c of constraints) { if (c.__type === 'orderBy') { out.sort((a, b) => { const av = a.data[c.field]; const bv = b.data[c.field]; if (av === bv) return 0; const cmp = av > bv ? 1 : -1; return c.dir === 'desc' ? -cmp : cmp; }); } else if (c.__type === 'limit') { out = out.slice(0, c.n); } else if (c.__type === 'where') { out = out.filter(d => d.data[c.field] === c.value); } } return out; } function clone(v) { if (v === null || v === undefined) return v; return JSON.parse(JSON.stringify(v)); }