Files
ng-eventually/packages/polyfill/test/use-shape-fake.ts
T
Sylvain Duchesne 3a8941159f fix: le filtre de lecture couvre l-enveloppe que le SDK renvoie vraiment
Le `useShape` publie enveloppait le resultat du hook dans le filtre ecrit pour
une COLLECTION. Depuis que `@ng-org/orm` renvoie un `UseShapeResult` autour du
set, `[...resultat]` et `.size` levaient, et `data`, `promise` et `subscription`
revenaient BRUTS - chacun tous les objets de toutes les identites, sans que rien
ne casse. Aucun faux de ce depot ne renvoyait la forme livree, donc aucune garde
ne pouvait le voir.

Deux vues au lieu d-une : celle de la collection, et celle de l-enveloppe que
`surface/use-shape.ts` applique. `data` et `promise` sont filtres, `subscription`
est refuse (c-est le handle de l-ORM, que cette surface ne publie pas), les deux
commandes de page passent.

Le defaut par TYPE et non par une liste de noms : un primitif ne peut atteindre
aucun objet et passe, un objet ou une fonction refuse tant qu-il n-existe pas de
forme filtree ici. C-est pourquoi `isLoading` n-est nomme nulle part, et pourquoi
un membre qu-une version suivante ajoutera est deja tranche. `size` et `length`
comptent ce que le porteur peut lire.

`UseShapeLike` etait `(...args: any[]) => any`, ce qui est la raison pour
laquelle les deux typechecks restaient a 0 pendant que la fuite etait ouverte :
aucune declaration ne disait ce que le hook rend. Il rend `ShapeResult`, releve
sur le `.d.ts` installe, et le vrai `reactUseShape` y est assignable sans cast.

Le faux partage `test/use-shape-fake.ts` renvoie la forme livree et c-est celui
que les deux gardes injectent - unitaire et e2e. La garantie du contrat et le
test qui l-epingle arrivent ensemble ; `1.0.0-dev.11`, majeur.
2026-09-24 18:27:02 +02:00

115 lines
5.0 KiB
TypeScript

/**
* use-shape-fake - a stand-in for the SDK's React hook, `@ng-org/orm`'s `useShape`.
*
* Not a `*.test.ts`, so `bun test` does not pick it up: it is the injection the suites
* share, and it is the seam the package is built on - an application hands `configure` the
* real hook, and a suite hands it this. Sibling of `orm-fake.ts`, which does the same for
* the shape query one floor down.
*
* -- Why it returns the ENVELOPE, and why that is the whole point -----------
* Every fake in this repository used to return a bare Set-like, because that is what the
* hook returned in `alpha.18`. `@ng-org/orm@0.1.2-alpha.21` returns a `UseShapeResult`
* AROUND the collection, and while no fake said so, a capability filter that handed three
* of that envelope's members back RAW could not fail a single test. So the shape below is
* read off the artifact that settles it - the installed package's
* `dist/frontendAdapters/react/useShape.d.ts`, and the `dist/.../useShape.js` beside it,
* which agree - and not off what this package wished the hook returned:
*
* { data, promise, subscription, isLoading, nextPage, previousPage }
*
* Each of the six is reproduced with the cardinality the declaration gives it:
* - `data` is a `Set` for an unordered config and an ARRAY once `orderBy` is asked for
* (upstream `SubscriptionData<T, CONF>`), and `undefined` while nothing has arrived;
* - `promise` resolves to that same collection, and is `undefined` when `conf` is;
* - `subscription` is the handle, carrying the items three ways of its own -
* `signalObject`, `readyPromise` and the raw arrays a change listener is handed;
* - `isLoading` is a boolean, the one member that carries nothing;
* - the two page commands exist only under a paginated config, as `WithMaybePagination`
* `Omit`s them otherwise and the shipped hook leaves them `undefined`.
*
* What it does NOT invent: no member the declaration does not carry. A fake richer than
* the target proves things about a surface nobody ships.
*/
import type { BaseType } from "@ng-org/shex-orm";
import type { ShapeResult, UseShapeLike } from "../src/model/types";
/** The subscription handle the envelope carries - the members `RdfOrmSubscription` exposes
* that reach the items, which is what makes handing it on a leak. */
export interface FakeSubscription {
readonly signalObject: unknown;
readonly readyPromise: Promise<unknown>;
readonly isReady: boolean;
addChangeListener(l: (c: { adds: unknown[]; updates: unknown[]; removes: unknown[] }) => void): void;
close(): void;
}
export interface FakeShapeHook {
/** What goes into `configure({ useShape })`. */
hook: UseShapeLike;
/** The rows the fake engine holds. A suite mutates this array in place. */
items: BaseType[];
/** The RAW collection the last call handed back - what a leak reaches, and what a test
* compares the filtered view against. */
raw(): unknown;
/** Pages asked for, in order, so a test can see a forwarded command arrive. */
pages: string[];
}
/**
* A fake `useShape` over an in-memory row list.
*
* `loaded` (the default) answers as a hook whose payload has arrived. Turn it off for the
* state the declaration describes as "still loading": `data` and `promise` present but the
* data not there yet.
*/
export function makeUseShapeHook(
options: { loaded?: boolean; paginated?: boolean } = {},
): FakeShapeHook {
const items: BaseType[] = [];
const pages: string[] = [];
const loaded = options.loaded !== false;
const paginated = options.paginated === true;
let lastRaw: unknown = undefined;
const hook: UseShapeLike = (_shapeType, conf): ShapeResult => {
// Upstream: "If the conf is explicitly set to `undefined`, the returned data is
// `undefined` and `isLoading` is false" - and `promise`/`subscription` are typed
// `CONF extends undefined ? undefined : ...`, so all three go together.
if (conf === undefined) {
lastRaw = undefined;
return {
data: undefined,
promise: undefined,
subscription: undefined,
isLoading: false,
nextPage: undefined,
previousPage: undefined,
};
}
// `SubscriptionData<T, CONF>`: a set with no `orderBy`, an ordered array with one.
const ordered = typeof conf === "object" && conf.orderBy !== undefined;
const collection: unknown = ordered ? [...items] : new Set(items);
lastRaw = collection;
const subscription: FakeSubscription = {
signalObject: collection,
readyPromise: Promise.resolve(collection),
isReady: loaded,
addChangeListener(l): void {
l({ adds: [...items], updates: [], removes: [] });
},
close(): void {},
};
return {
data: loaded ? collection : undefined,
promise: Promise.resolve(collection),
subscription,
isLoading: !loaded,
nextPage: paginated ? (): void => void pages.push("next") : undefined,
previousPage: paginated ? (): void => void pages.push("previous") : undefined,
};
};
return { hook, items, raw: () => lastRaw, pages };
}