Initial Commit
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
# `runBackendConformance`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## 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
|
||||
|
||||
```ts
|
||||
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.
|
||||
|
||||
```ts
|
||||
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`
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user