Files
2026-05-27 23:47:53 +01:00

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, one it(...) per universal contract clause.
  • Capability-gated assertions: readMulti / save / lastModified tests early-return for backends that do not declare the corresponding capability flag, keeping the suite green for read-only backends like HTTP.
  • Optional seed adapter on the factory return so backends with save can pre-populate without exposing internals; backends without save (HTTP-style) seed via their constructor options instead.
  • Runner-neutral: imports describe / it / expect from @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 outer describe(...) title.
  • makeFactory: () => Promise<ConformanceContext> — Async factory invoked once per it(...); produces a fresh backend instance and optional seed callback. Always producing a fresh backend per call prevents cross-test state leakage.
  • Returns: void. The harness registers describe / it blocks 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 Backend interface from @suaveplan/types, never from @suaveplan/i18n core. Backends consume the same type from the same location, so the harness adds zero coupling to the slim core.
  • lastModified monotonicity uses a 2-ms setTimeout between the two save calls to defend against backends whose injected clock has 1-ms resolution. Backends with deterministic counters always satisfy t2 >= 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.