4.7 KiB
runBackendConformance
(no submodules)
Purpose
runBackendConformance is the shared, runner-neutral conformance
harness every concrete @suaveplan/i18n-backend-* package runs
against its Backend implementation. It exists so the universal
contract — name + capabilities surface, read-of-absent → typed
I18nError, optional readMulti merge, optional save round-trip,
optional lastModified monotonicity — gets one canonical test body
that every backend re-uses verbatim. New backends inherit the
guarantees automatically; no copy-pasted test code drifting between
implementations.
The harness lives under the @suaveplan/i18n-shared/testing subpath
so backend test files can import it without pulling the full i18n
core or any other backend. It deliberately uses
@suaveplan/testing/runner so it executes identically under Vitest
and Bun's test runner.
Features
- One
describe(...)per backend, oneit(...)per universal contract clause. - Capability-gated assertions:
readMulti/save/lastModifiedtests early-return for backends that do not declare the corresponding capability flag, keeping the suite green for read-only backends like HTTP. - Optional
seedadapter on the factory return so backends withsavecan pre-populate without exposing internals; backends withoutsave(HTTP-style) seed via their constructor options instead. - Runner-neutral: imports
describe/it/expectfrom@suaveplan/testing/runner, so the same harness runs under Vitest or Bun's test runner without modification.
Usage: Basic Example
import { runBackendConformance } from "@suaveplan/i18n-shared/testing";
import { MemoryBackend } from "@suaveplan/i18n-backend-memory";
runBackendConformance("MemoryBackend", async () => {
const backend = new MemoryBackend();
return {
backend,
async seed(locale, namespace, record) {
await backend.save(locale, namespace, record as never);
},
};
});
Usage: Advanced Example
A read-only backend (no save, no readMulti) supplies its own
seeding via constructor options and omits seed from the factory
return. The harness inspects backend.capabilities and the
structural presence of optional methods to decide which assertions
to run; unsupported assertions exit early.
import { runBackendConformance } from "@suaveplan/i18n-shared/testing";
import { httpBackend } from "@suaveplan/i18n-backend-http";
runBackendConformance("HttpBackend (seeded fetch)", async () => {
const backend = httpBackend({
loadPath: "https://i18n.example/{{locale}}/{{ns}}.json",
fetch: async () =>
new Response(JSON.stringify({ login: { title: "Sign in" } })),
});
return { backend };
});
API Reference
runBackendConformance(label, makeFactory)
label: string— Human-readable backend label inserted into the outerdescribe(...)title.makeFactory: () => Promise<ConformanceContext>— Async factory invoked once perit(...); produces a fresh backend instance and optionalseedcallback. Always producing a fresh backend per call prevents cross-test state leakage.- Returns:
void. The harness registersdescribe/itblocks with the active runner; assertions execute when the runner runs the file.
ConformanceContext
interface ConformanceContext {
readonly backend: Backend; // from @suaveplan/types
readonly seed?: (
locale: string,
namespace: string,
record: Record<string, unknown>
) => Promise<void>;
}
ConformanceFactory
type ConformanceFactory = () => Promise<ConformanceContext>;
Implementation Notes
- The harness gates each capability check on both the structural
presence of the method (
backend.save) and the corresponding capability flag (backend.capabilities.canSave). This catches backends whose flags drift out of sync with their methods at authoring time and keeps the harness honest for partial implementations. - The harness imports the
Backendinterface from@suaveplan/types, never from@suaveplan/i18ncore. Backends consume the same type from the same location, so the harness adds zero coupling to the slim core. lastModifiedmonotonicity uses a 2-mssetTimeoutbetween the twosavecalls to defend against backends whose injected clock has 1-ms resolution. Backends with deterministic counters always satisfyt2 >= t1.- This harness is the Wave 5 sequencing keystone: the five backend
packages (
memory,http,storage,fs-multifile,chained) all call it. Lifting it here lets every backend land independently without copy-pasting test fixtures.