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.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { getCaps } from "../src/shared-wallet/bootstrap";
|
||||
import { makeUseShapeHook } from "./use-shape-fake";
|
||||
import { test, expect, mock, afterEach } from "bun:test";
|
||||
import { makeNg } from "../src/surface/ng-proxy";
|
||||
import { configure } from "../src/index";
|
||||
@@ -126,7 +127,7 @@ function asyncProxyNg(calls: string[]): Record<string, unknown> {
|
||||
test("passthrough: a member of the REAL (proxy-shaped) `ng` comes back callable", async () => {
|
||||
const calls: string[] = [];
|
||||
const ng = asyncProxyNg(calls);
|
||||
configure({ ng, useShape: () => undefined });
|
||||
configure({ ng, useShape: makeUseShapeHook().hook });
|
||||
const proxy = makeNg();
|
||||
|
||||
expect(typeof proxy.orm_start_graph).toBe("function");
|
||||
@@ -145,7 +146,7 @@ test("passthrough: a plain-object `ng` keeps its receiver", async () => {
|
||||
return this.mark;
|
||||
},
|
||||
};
|
||||
configure({ ng, useShape: () => undefined });
|
||||
configure({ ng, useShape: makeUseShapeHook().hook });
|
||||
const proxy = makeNg() as unknown as { whoAmI: () => string };
|
||||
expect(proxy.whoAmI()).toBe("the real ng");
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
*/
|
||||
|
||||
import { getCaps } from "../src/shared-wallet/bootstrap";
|
||||
import { makeUseShapeHook } from "./use-shape-fake";
|
||||
import { test, expect, mock, afterAll } from "bun:test";
|
||||
import { readDocumentSubjects } from "../src/surface/read-document";
|
||||
import type { Nuri } from "../src/model/types";
|
||||
@@ -230,7 +231,7 @@ function injectTriples(triplesByDoc: Record<string, Array<[string, string, strin
|
||||
resetOpenedRepos();
|
||||
resetPublicStoreFetches();
|
||||
setCurrentUser(null);
|
||||
configure({ ng, useShape: () => undefined });
|
||||
configure({ ng, useShape: makeUseShapeHook().hook });
|
||||
configureStoreRegistry({
|
||||
getSession: async () => ({ sessionId: "sid-rd", privateStoreId: "priv" }),
|
||||
normalizeId: (u: string) => u,
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* The published `useShape` - what an identity may see of what the SDK handed back.
|
||||
*
|
||||
* -- Why this file exists, and what it is the answer to --------------------
|
||||
* One shared wallet hosts every identity, so the hook's own answer is every identity's
|
||||
* objects and the capability filter is the whole of the isolation. Until 2026-09-24 that
|
||||
* filter was written for the collection `@ng-org/orm@0.1.2-alpha.18` returned, while the
|
||||
* version this package builds against - `alpha.21` - returns a `UseShapeResult` ENVELOPE
|
||||
* around it. Three plain members of that envelope (`data`, `promise`, `subscription`) each
|
||||
* carried the items and each came back RAW, and no test in this repository could see it,
|
||||
* because no fake returned the shape the SDK ships. `test/use-shape-fake.ts` now does, read
|
||||
* off the installed package's own `.d.ts`, and this file is what it made possible.
|
||||
*
|
||||
* Every document below is OBTAINED through the published placement call under the identity
|
||||
* that owns it - never named by the test - so "hers" and "his" are facts of the system.
|
||||
* Nothing hands a `ReadCap` across the boundary: the stranger holds what a stranger holds.
|
||||
*/
|
||||
import { test, expect, afterEach } from "bun:test";
|
||||
import { configure, storeRegistry, useShape } from "../src/index";
|
||||
import {
|
||||
configureStoreRegistry,
|
||||
resetCaps,
|
||||
resetConfig,
|
||||
resetStoreRegistry,
|
||||
setCurrentUser,
|
||||
} from "../src/shared-wallet/bootstrap";
|
||||
import { resetRegistryCache } from "../src/shared-wallet/account-registry";
|
||||
import { makeWallet, SESSION, type Quad } from "./wallet-fake";
|
||||
import { makeUseShapeHook } from "./use-shape-fake";
|
||||
import type { FakeShapeHook } from "./use-shape-fake";
|
||||
import type { NgLike, Nuri, ShapeResult, UseShapeLike } from "../src/model/types";
|
||||
import type { BaseType, ShapeType } from "@ng-org/shex-orm";
|
||||
// TYPE-only: no `@ng-org/orm` module is loaded at runtime here. What this line checks is
|
||||
// the claim the injection type MAKES - that the SDK's OWN React hook is assignable to
|
||||
// `UseShapeLike` as it stands, with no wrapper and no cast, so `configure({ useShape })`
|
||||
// is the whole of what a caller writes. It is also what keeps that type honest: it was
|
||||
// `(...args: any[]) => any`, which no shape could ever contradict.
|
||||
import type { reactUseShape } from "@ng-org/orm/all";
|
||||
type _TheSdkHookFits = typeof reactUseShape extends UseShapeLike ? true : never;
|
||||
const _sdkFits: _TheSdkHookFits = true;
|
||||
void _sdkFits;
|
||||
|
||||
const NoteShape = {
|
||||
shape: "urn:use-shape-test:NoteShape",
|
||||
schema: {
|
||||
"urn:use-shape-test:NoteShape": {
|
||||
iri: "urn:use-shape-test:NoteShape",
|
||||
predicates: [
|
||||
{
|
||||
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
|
||||
readablePredicate: "@type",
|
||||
dataTypes: [{ valType: "iri", literals: ["urn:use-shape-test:Note"] }],
|
||||
minCardinality: 1,
|
||||
maxCardinality: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} satisfies ShapeType<BaseType>;
|
||||
|
||||
/** Everything the local dataset holds - the scope under which this filter is the only
|
||||
* thing between the caller and every identity's objects. */
|
||||
const EVERYTHING = { graphs: "did:ng:i" };
|
||||
|
||||
function inject(hook: FakeShapeHook): void {
|
||||
const quads: Quad[] = [];
|
||||
const ng: NgLike = makeWallet(quads);
|
||||
configure({ ng, useShape: hook.hook });
|
||||
configureStoreRegistry({ getSession: async () => SESSION, normalizeId: (id) => id.trim() });
|
||||
resetRegistryCache();
|
||||
resetCaps();
|
||||
setCurrentUser(null);
|
||||
}
|
||||
|
||||
/** A document that really belongs to `who` - created by them, through the published call,
|
||||
* which is also what files its cap in their keyring. */
|
||||
async function documentOf(who: string): Promise<Nuri> {
|
||||
setCurrentUser(who);
|
||||
return storeRegistry.createEntityDoc(await storeRegistry.protectedStore());
|
||||
}
|
||||
|
||||
/** One ORM object placed in `doc`, as the ORM itself carries it. */
|
||||
function noteIn(doc: Nuri, id: string): BaseType {
|
||||
return { "@id": `${doc}:q:${id}`, "@graph": doc } as BaseType;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
resetConfig();
|
||||
resetStoreRegistry();
|
||||
resetCaps();
|
||||
setCurrentUser(null);
|
||||
});
|
||||
|
||||
// -- THE RIGHTS CASE: the stranger ------------------------------------------
|
||||
|
||||
// The defect, stated as a test. Each of the three members below is one the `alpha.21`
|
||||
// declaration gives the items to, and each used to hand back the unfiltered collection.
|
||||
test("useShape: no member of the result reaches a document this identity holds no cap for", async () => {
|
||||
const hook = makeUseShapeHook();
|
||||
inject(hook);
|
||||
const hers = await documentOf("alice");
|
||||
const his = await documentOf("bob");
|
||||
hook.items.push(noteIn(hers, "a1"), noteIn(his, "b1"));
|
||||
|
||||
setCurrentUser("bob");
|
||||
const result: ShapeResult = useShape(NoteShape, EVERYTHING);
|
||||
|
||||
// `data` - what the SDK's own example destructures first.
|
||||
expect([...(result.data as Iterable<BaseType>)].map((o) => o["@graph"])).toEqual([his]);
|
||||
expect((result.data as { size: number }).size).toBe(1);
|
||||
// `promise` - the same collection, one await away.
|
||||
expect([...((await result.promise) as Iterable<BaseType>)].map((o) => o["@graph"])).toEqual([his]);
|
||||
// `subscription` - the SDK's own handle, which reads and writes with no filter at all.
|
||||
expect(() => result.subscription).toThrow(/RdfOrmSubscription/);
|
||||
|
||||
// …and the collection underneath really does hold both, so it is the filter that
|
||||
// removed one rather than the fake never having offered it.
|
||||
expect([...(hook.raw() as Iterable<BaseType>)]).toHaveLength(2);
|
||||
});
|
||||
|
||||
// Iterating is not the only way in, and the members a reactive set puts forward are the
|
||||
// ones a consumer reaches for first. Same rule as the collection view's own regression.
|
||||
test("useShape: the item-yielding members of `data` are filtered too, not just iteration", async () => {
|
||||
const hook = makeUseShapeHook();
|
||||
inject(hook);
|
||||
const hers = await documentOf("alice");
|
||||
const his = await documentOf("bob");
|
||||
const foreign = noteIn(hers, "a1");
|
||||
hook.items.push(foreign, noteIn(his, "b1"));
|
||||
|
||||
setCurrentUser("bob");
|
||||
const data = useShape(NoteShape, EVERYTHING).data as {
|
||||
values(): Iterable<BaseType>;
|
||||
forEach(cb: (v: BaseType) => void): void;
|
||||
has(v: BaseType): boolean;
|
||||
};
|
||||
expect([...data.values()].map((o) => o["@graph"])).toEqual([his]);
|
||||
const seen: string[] = [];
|
||||
data.forEach((o) => seen.push(o["@graph"]));
|
||||
expect(seen).toEqual([his]);
|
||||
expect(data.has(foreign)).toBe(false);
|
||||
});
|
||||
|
||||
// -- THE OTHER DIRECTION: the holder is served ------------------------------
|
||||
|
||||
// A repair that closes a leak by making the call unusable has traded one defect for
|
||||
// another. Every member the declaration offers a holder is exercised here on their OWN
|
||||
// data, and the two page commands with it.
|
||||
test("useShape: the holder reads their own items through every member the SDK offers", async () => {
|
||||
const hook = makeUseShapeHook({ paginated: true });
|
||||
inject(hook);
|
||||
const hers = await documentOf("alice");
|
||||
const his = await documentOf("bob");
|
||||
hook.items.push(noteIn(hers, "a1"), noteIn(his, "b1"), noteIn(hers, "a2"));
|
||||
|
||||
setCurrentUser("alice");
|
||||
const result: ShapeResult = useShape(NoteShape, EVERYTHING);
|
||||
|
||||
expect([...(result.data as Iterable<BaseType>)].map((o) => o["@graph"])).toEqual([hers, hers]);
|
||||
expect((result.data as { size: number }).size).toBe(2);
|
||||
expect([...((await result.promise) as Iterable<BaseType>)]).toHaveLength(2);
|
||||
// The load state is a boolean and passes on its own, with no entry naming it.
|
||||
expect(result.isLoading).toBe(false);
|
||||
// The page commands forward: they return `void`, and what they load lands in a
|
||||
// collection only ever read back through the filter.
|
||||
result.nextPage?.();
|
||||
result.previousPage?.();
|
||||
expect(hook.pages).toEqual(["next", "previous"]);
|
||||
// Reading `data` twice hands back the same view, so a caller may put it in a
|
||||
// dependency array without it looking like new data on every read.
|
||||
expect(result.data).toBe(result.data);
|
||||
});
|
||||
|
||||
// Before the first cap exists the emulation is not in force, exactly as `getObjects` and
|
||||
// `useShapeQuery` behave: a consumer that never touches caps keeps reading everything,
|
||||
// and gets the SDK's own object back untouched.
|
||||
test("useShape: passthrough while no cap has been issued", () => {
|
||||
const hook = makeUseShapeHook();
|
||||
inject(hook);
|
||||
hook.items.push(noteIn("did:ng:o:someone" as Nuri, "x"));
|
||||
const result: ShapeResult = useShape(NoteShape, EVERYTHING);
|
||||
expect([...(result.data as Iterable<BaseType>)]).toHaveLength(1);
|
||||
expect(result.subscription).not.toBeUndefined();
|
||||
});
|
||||
|
||||
// Upstream: "If the conf is explicitly set to `undefined`, the returned data is
|
||||
// `undefined` and `isLoading` is false". Nothing was opened, so there is nothing to
|
||||
// refuse, and `subscription` answers the SDK's own value rather than throwing.
|
||||
test("useShape: an undefined config answers undefined rather than refusing", async () => {
|
||||
const hook = makeUseShapeHook();
|
||||
inject(hook);
|
||||
await documentOf("alice");
|
||||
setCurrentUser("alice");
|
||||
const result: ShapeResult = useShape(NoteShape, undefined);
|
||||
expect(result.data).toBeUndefined();
|
||||
expect(result.promise).toBeUndefined();
|
||||
expect(result.subscription).toBeUndefined();
|
||||
expect(result.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
// -- SAFE FOR A SHAPE NOBODY HAS SEEN --------------------------------------
|
||||
|
||||
/** What a later `@ng-org/orm` might hand back: the six members of `alpha.21` plus two
|
||||
* nobody has declared - one holding the collection, one holding a count. */
|
||||
interface LaterResult extends ShapeResult {
|
||||
readonly rows: unknown;
|
||||
readonly totalCount: number;
|
||||
readonly sample: () => unknown;
|
||||
}
|
||||
|
||||
// The whole point of deciding by TYPE rather than by a list of names. A name list is what
|
||||
// breaks silently on the next bump - which is exactly how this defect arrived - so the
|
||||
// question asked of an unnamed member is what no version number can change: can the value
|
||||
// behind it reach an item at all?
|
||||
test("useShape: a member no version has declared yet cannot leak, while a scalar one still passes", async () => {
|
||||
const base = makeUseShapeHook();
|
||||
const later: FakeShapeHook = {
|
||||
...base,
|
||||
hook: ((shapeType, conf) => {
|
||||
const result: LaterResult = {
|
||||
...base.hook(shapeType, conf),
|
||||
rows: base.raw(),
|
||||
totalCount: 7,
|
||||
sample: () => [...(base.raw() as Iterable<BaseType>)][0],
|
||||
};
|
||||
return result;
|
||||
}) as UseShapeLike,
|
||||
};
|
||||
inject(later);
|
||||
const hers = await documentOf("alice");
|
||||
const his = await documentOf("bob");
|
||||
base.items.push(noteIn(hers, "a1"), noteIn(his, "b1"));
|
||||
|
||||
setCurrentUser("bob");
|
||||
const result = useShape(NoteShape, EVERYTHING) as LaterResult;
|
||||
|
||||
// An OBJECT member nobody named holds the collection, so it is refused at the read -
|
||||
// there is no later moment to refuse it at.
|
||||
expect(() => result.rows).toThrow(/not filtered/i);
|
||||
// A FUNCTION member nobody named is refused at the CALL, so reading it (a logger, a
|
||||
// `typeof` guard) is not itself refused.
|
||||
expect(typeof result.sample).toBe("function");
|
||||
expect(() => result.sample()).toThrow(/not filtered/i);
|
||||
// A PRIMITIVE cannot hold a reference to anything, so it passes with no entry of its
|
||||
// own - which is why `isLoading` needs none either.
|
||||
expect(result.totalCount).toBe(7);
|
||||
});
|
||||
|
||||
// An `orderBy` config makes the collection an ARRAY (upstream `SubscriptionData`), whose
|
||||
// count is spelled `length` and is a number - so it used to pass as a harmless primitive
|
||||
// while publishing how many items the holder may NOT read.
|
||||
test("useShape: an ordered result does not publish the unfiltered count", async () => {
|
||||
const hook = makeUseShapeHook();
|
||||
inject(hook);
|
||||
const hers = await documentOf("alice");
|
||||
const his = await documentOf("bob");
|
||||
hook.items.push(noteIn(hers, "a1"), noteIn(his, "b1"), noteIn(his, "b2"));
|
||||
|
||||
setCurrentUser("alice");
|
||||
const data = useShape(NoteShape, { ...EVERYTHING, orderBy: { liveMark: "asc" } }).data;
|
||||
expect(Array.isArray(hook.raw())).toBe(true);
|
||||
expect((hook.raw() as BaseType[]).length).toBe(3);
|
||||
expect((data as { length: number }).length).toBe(1);
|
||||
expect([...(data as Iterable<BaseType>)].map((o) => o["@graph"])).toEqual([hers]);
|
||||
// An index hands back a raw item and there is no projection of it this file would not
|
||||
// have to invent, so it is refused rather than guessed at.
|
||||
expect(() => (data as Record<number, unknown>)[0]).toThrow(/not filtered/i);
|
||||
});
|
||||
Reference in New Issue
Block a user