2026-06-28 17:59:50 -04:00
|
|
|
// 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 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 });
|
2026-07-04 12:22:04 -04:00
|
|
|
if (opts && opts.merge) {
|
|
|
|
|
MOCK_DB.merge(docRef.path, clone(data));
|
|
|
|
|
} else {
|
|
|
|
|
MOCK_DB.set(docRef.path, clone(data));
|
|
|
|
|
}
|
2026-06-28 17:59:50 -04:00
|
|
|
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}`;
|
2026-06-28 19:00:08 -04:00
|
|
|
recordCall({ fn: 'addDoc', path, data: clone(data) });
|
2026-06-28 17:59:50 -04:00
|
|
|
MOCK_DB.set(path, clone(data));
|
|
|
|
|
return { id, path };
|
|
|
|
|
}
|
2026-06-28 18:12:27 -04:00
|
|
|
export function writeBatch(db) {
|
2026-06-28 17:59:50 -04:00
|
|
|
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;
|
|
|
|
|
const docs = MOCK_DB.collection(collPath);
|
2026-06-28 19:00:08 -04:00
|
|
|
return { docs: docs.map(d => ({ id: d.id, data: () => d.data, ref: { path: `${collPath}/${d.id}` } })) };
|
2026-06-28 17:59:50 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// realtime — emit from mock DB, capture unsub
|
|
|
|
|
export function onSnapshot(refOrQuery, onSuccess, onError) {
|
|
|
|
|
const path = refOrQuery.path || (refOrQuery.ref && refOrQuery.ref.path);
|
2026-07-04 12:22:04 -04:00
|
|
|
const constraints = refOrQuery.constraints || [];
|
2026-06-28 17:59:50 -04:00
|
|
|
// 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 {
|
2026-07-04 12:22:04 -04:00
|
|
|
let docs = MOCK_DB.collection(path);
|
|
|
|
|
docs = applyConstraints(docs, constraints);
|
2026-06-28 17:59:50 -04:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-04 12:22:04 -04:00
|
|
|
// Apply Firestore-style query constraints (orderBy desc/asc, limit) to mock docs.
|
|
|
|
|
// Mirrors real SDK semantics enough for contract tests. Only orderBy + limit
|
|
|
|
|
// supported (App's LOG_QUERY uses exactly these).
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-28 17:59:50 -04:00
|
|
|
function clone(v) {
|
|
|
|
|
if (v === null || v === undefined) return v;
|
|
|
|
|
return JSON.parse(JSON.stringify(v));
|
|
|
|
|
}
|