@burojs/test-helpers 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,60 @@
1
+ import { BatchedQueryOperation, DataProvider, ErasedMutationDescriptor, ErasedQueryDescriptor, validateBatchResult, validateMutationResult } from "@burojs/core";
2
+ //#region src/data-provider-contract.d.ts
3
+ type QueryKind = ErasedQueryDescriptor['kind'];
4
+ type MutationKind = ErasedMutationDescriptor['kind'];
5
+ interface ProviderObservedCall {
6
+ readonly lifecycle: 'query' | 'mutation' | 'batch' | 'live-open' | 'live-subscribe';
7
+ readonly kind?: QueryKind | MutationKind;
8
+ readonly descriptor?: ErasedQueryDescriptor | ErasedMutationDescriptor;
9
+ readonly descriptors?: readonly ErasedQueryDescriptor[];
10
+ readonly signal?: AbortSignal;
11
+ }
12
+ /** Specimens supplied by an adapter author; unsupported optional methods are omitted. */
13
+ interface ProviderContractDescriptors {
14
+ readonly queries?: Partial<Record<QueryKind, ErasedQueryDescriptor>>;
15
+ readonly mutations?: Partial<Record<MutationKind, ErasedMutationDescriptor>>;
16
+ readonly batch?: readonly BatchedQueryOperation[];
17
+ /** Independent batch groups prove that grouping is explicit, never merged by the helper. */
18
+ readonly batchGroups?: readonly (readonly BatchedQueryOperation[])[];
19
+ /** List specimens for both pagination protocols, including expected totals/pageInfo. */
20
+ readonly pagination?: {
21
+ readonly offset?: ErasedQueryDescriptor;
22
+ readonly cursor?: ErasedQueryDescriptor;
23
+ };
24
+ readonly live?: ErasedQueryDescriptor;
25
+ /** Error specimens must make the selected handler reject with a DataError. */
26
+ readonly errors?: readonly ProviderContractErrorSpecimen[];
27
+ /** Hand-authored mapped results; assertions never derive expectations from an adapter. */
28
+ readonly expected?: {
29
+ readonly queries?: Partial<Record<QueryKind, unknown>>;
30
+ readonly mutations?: Partial<Record<MutationKind, unknown>>;
31
+ readonly batch?: unknown;
32
+ readonly pagination?: {
33
+ readonly offset?: unknown;
34
+ readonly cursor?: unknown;
35
+ };
36
+ };
37
+ }
38
+ type ProviderContractErrorSpecimen = {
39
+ readonly lifecycle: 'query';
40
+ readonly kind: QueryKind;
41
+ readonly descriptor: ErasedQueryDescriptor;
42
+ } | {
43
+ readonly lifecycle: 'mutation';
44
+ readonly kind: MutationKind;
45
+ readonly descriptor: ErasedMutationDescriptor;
46
+ };
47
+ interface ProviderContractFixture {
48
+ create(): DataProvider | Promise<DataProvider>;
49
+ descriptors: ProviderContractDescriptors;
50
+ observeCalls(): readonly ProviderObservedCall[];
51
+ }
52
+ /**
53
+ * Registers transport-agnostic provider conformance tests. It intentionally
54
+ * imports only core contracts: adapters supply their own deterministic client
55
+ * and expose provider-boundary observations through the fixture.
56
+ */
57
+ declare function dataProviderContract(name: string, fixture: ProviderContractFixture): void;
58
+ //#endregion
59
+ export { type ProviderContractDescriptors, type ProviderContractFixture, type ProviderObservedCall, dataProviderContract };
60
+ //# sourceMappingURL=provider-contract.d.ts.map
@@ -0,0 +1,272 @@
1
+ import { isDataError, resolveDataCapability, validateBatchResult, validateMutationResult } from "@burojs/core";
2
+ import { describe, expect, it } from "vitest";
3
+ //#region src/data-provider-contract.ts
4
+ const SESSION = Object.freeze({
5
+ sessionKey: "contract-session",
6
+ scopeKey: "contract-scope",
7
+ generation: 1
8
+ });
9
+ const REQUEST_CONTEXT = Object.freeze({ contract: "provider" });
10
+ function context(signal) {
11
+ return Object.freeze({
12
+ signal,
13
+ requestContext: REQUEST_CONTEXT,
14
+ session: SESSION,
15
+ providerServiceKey: "contract-provider"
16
+ });
17
+ }
18
+ function queryShape(kind, value) {
19
+ if (kind === "custom") return;
20
+ expect(value).toBeTypeOf("object");
21
+ if (kind === "list" || kind === "many") expect(value.data).toBeInstanceOf(Array);
22
+ if (kind === "one") expect(value).toHaveProperty("data");
23
+ if (kind === "count") expect([null, "number"]).toContain(typeof value.total);
24
+ }
25
+ function observed(fixture, expected, signal) {
26
+ expect(fixture.observeCalls()).toContainEqual(expect.objectContaining({
27
+ ...expected,
28
+ signal
29
+ }));
30
+ }
31
+ async function assertBatch(provider, fixture, operations, expected) {
32
+ expect(provider.batch?.queries).toBeTypeOf("function");
33
+ const controller = new AbortController();
34
+ const result = await provider.batch.queries(operations, context(controller.signal));
35
+ validateBatchResult(operations, result);
36
+ if (expected !== void 0) expect(result).toEqual(expected);
37
+ for (const entry of result.results) if (entry.status === "error") dataError(entry.error);
38
+ observed(fixture, {
39
+ lifecycle: "batch",
40
+ descriptors: operations.map((operation) => operation.descriptor)
41
+ }, controller.signal);
42
+ }
43
+ function dataError(value) {
44
+ expect(isDataError(value)).toBe(true);
45
+ }
46
+ function livePlan(value) {
47
+ expect(value, "descriptors.live was supplied, so live.open must return a plan").not.toBeNull();
48
+ expect(value).toBeTypeOf("object");
49
+ expect(value?.subscribe).toBeTypeOf("function");
50
+ }
51
+ function liveDescriptorRequired(hasLiveSlot, descriptor) {
52
+ if (!hasLiveSlot) return;
53
+ expect(descriptor, "the provider exposes a `live` slot, so the fixture must supply `descriptors.live` — omitting it silently skips the whole live cell").toBeDefined();
54
+ }
55
+ function cursorDeclarationRequired(hasListSlot, descriptors) {
56
+ if (!hasListSlot) return;
57
+ const refusalSpecimen = (descriptors.errors ?? []).some((specimen) => specimen.lifecycle === "query" && specimen.kind === "list" && specimen.descriptor.pagination?.kind === "cursor");
58
+ expect(descriptors.pagination?.cursor !== void 0 || refusalSpecimen, "the provider exposes a `list` slot, so the fixture must state its cursor-pagination position: supply `descriptors.pagination.cursor` for a provider that serves cursor pages, or an `errors` specimen holding a cursor list descriptor for one that refuses. Saying nothing used to pass this cell for a provider whose cursor paging was entirely broken.").toBe(true);
59
+ }
60
+ function cursorPageShape(value) {
61
+ const pageInfo = value.pageInfo;
62
+ expect(pageInfo, "a cursor list must return `pageInfo` — without it there is nothing to page with").toBeTypeOf("object");
63
+ const next = pageInfo?.nextCursor;
64
+ if (next !== void 0) expect(next).toBeTypeOf("string");
65
+ const has = pageInfo?.hasNextPage;
66
+ if (has !== void 0) expect(has).toBeTypeOf("boolean");
67
+ }
68
+ function cursorPagesRequired(value) {
69
+ const pageInfo = value.pageInfo;
70
+ expect(pageInfo?.hasNextPage, "a supplied `descriptors.pagination.cursor` declares that this descriptor PAGES, so its first page must report `hasNextPage: true`. Size the descriptor `limit` below the fixture row count; a provider that cannot page belongs in an `errors` refusal specimen instead. (A first page that reports itself finished used to skip the whole second-page half of this cell.)").toBe(true);
71
+ const next = pageInfo?.nextCursor;
72
+ expect(typeof next === "string" && next.length > 0, "a first page reporting `hasNextPage: true` must also mint a non-empty `nextCursor` — it is the only thing a caller can hand back, and without it the infinite-list observer stops paging").toBe(true);
73
+ }
74
+ Object.freeze({
75
+ queryShape,
76
+ observed,
77
+ batch: validateBatchResult,
78
+ mutation: validateMutationResult,
79
+ dataError,
80
+ livePlan,
81
+ liveDescriptorRequired,
82
+ cursorDeclarationRequired,
83
+ cursorPageShape,
84
+ cursorPagesRequired
85
+ });
86
+ function dataProviderContract(name, fixture) {
87
+ describe(`${name} data provider contract`, () => {
88
+ for (const kind of [
89
+ "list",
90
+ "one",
91
+ "many",
92
+ "count",
93
+ "custom"
94
+ ]) it(`preserves the ${kind} query descriptor and AbortSignal`, async () => {
95
+ const provider = await fixture.create();
96
+ const descriptor = fixture.descriptors.queries?.[kind];
97
+ const handler = provider.queries?.[kind];
98
+ if (descriptor === void 0 || handler === void 0) return;
99
+ const controller = new AbortController();
100
+ const result = await handler(descriptor, context(controller.signal));
101
+ queryShape(kind, result);
102
+ const expected = fixture.descriptors.expected?.queries?.[kind];
103
+ if (expected !== void 0) expect(result).toEqual(expected);
104
+ observed(fixture, {
105
+ lifecycle: "query",
106
+ kind,
107
+ descriptor
108
+ }, controller.signal);
109
+ });
110
+ for (const kind of [
111
+ "createOne",
112
+ "createMany",
113
+ "updateOne",
114
+ "updateMany",
115
+ "deleteOne",
116
+ "deleteMany",
117
+ "custom"
118
+ ]) it(`validates ${kind} mutation output and preserves its request`, async () => {
119
+ const provider = await fixture.create();
120
+ const descriptor = fixture.descriptors.mutations?.[kind];
121
+ const handler = provider.mutations?.[kind];
122
+ if (descriptor === void 0 || handler === void 0) return;
123
+ const controller = new AbortController();
124
+ const result = await handler(descriptor, context(controller.signal));
125
+ validateMutationResult(descriptor, result);
126
+ const expected = fixture.descriptors.expected?.mutations?.[kind];
127
+ if (expected !== void 0) expect(result).toEqual(expected);
128
+ observed(fixture, {
129
+ lifecycle: "mutation",
130
+ kind,
131
+ descriptor
132
+ }, controller.signal);
133
+ });
134
+ for (const specimen of fixture.descriptors.errors ?? []) it(`returns a structured error for the ${specimen.lifecycle}.${specimen.kind} error specimen`, async () => {
135
+ const provider = await fixture.create();
136
+ const controller = new AbortController();
137
+ const handler = specimen.lifecycle === "query" ? provider.queries?.[specimen.kind] : provider.mutations?.[specimen.kind];
138
+ expect(handler).toBeTypeOf("function");
139
+ try {
140
+ await handler(specimen.descriptor, context(controller.signal));
141
+ throw new Error("Provider error specimen resolved instead of rejecting.");
142
+ } catch (error) {
143
+ dataError(error);
144
+ }
145
+ observed(fixture, {
146
+ lifecycle: specimen.lifecycle,
147
+ kind: specimen.kind,
148
+ descriptor: specimen.descriptor
149
+ }, controller.signal);
150
+ });
151
+ it("reports absent optional capabilities without invoking fallback handlers", async () => {
152
+ const provider = await fixture.create();
153
+ const before = fixture.observeCalls().length;
154
+ for (const kind of [
155
+ "list",
156
+ "one",
157
+ "many",
158
+ "count",
159
+ "custom"
160
+ ]) if (provider.queries?.[kind] === void 0) {
161
+ const capability = resolveDataCapability(provider, {
162
+ lifecycle: "query",
163
+ kind
164
+ });
165
+ expect(capability).toMatchObject({
166
+ kind: "capability",
167
+ code: "missing-data-capability"
168
+ });
169
+ }
170
+ for (const kind of [
171
+ "createOne",
172
+ "createMany",
173
+ "updateOne",
174
+ "updateMany",
175
+ "deleteOne",
176
+ "deleteMany",
177
+ "custom"
178
+ ]) if (provider.mutations?.[kind] === void 0) {
179
+ const capability = resolveDataCapability(provider, {
180
+ lifecycle: "mutation",
181
+ kind
182
+ });
183
+ expect(capability).toMatchObject({
184
+ kind: "capability",
185
+ code: "missing-data-capability"
186
+ });
187
+ }
188
+ expect(fixture.observeCalls()).toHaveLength(before);
189
+ });
190
+ it("validates batch IDs, kinds, partial errors, and the shared AbortSignal", async () => {
191
+ const provider = await fixture.create();
192
+ const operations = fixture.descriptors.batch;
193
+ if (provider.batch?.queries === void 0 || operations === void 0) return;
194
+ await assertBatch(provider, fixture, operations, fixture.descriptors.expected?.batch);
195
+ });
196
+ for (const [index, operations] of (fixture.descriptors.batchGroups ?? []).entries()) it(`keeps batch group ${index + 1} isolated and validates its partial results`, async () => {
197
+ await assertBatch(await fixture.create(), fixture, operations, void 0);
198
+ });
199
+ it("preserves offset pagination and its mapped total/page shape", async () => {
200
+ const provider = await fixture.create();
201
+ const descriptor = fixture.descriptors.pagination?.offset;
202
+ if (descriptor === void 0 || provider.queries?.list === void 0) return;
203
+ const controller = new AbortController();
204
+ const result = await provider.queries.list(descriptor, context(controller.signal));
205
+ queryShape("list", result);
206
+ const expected = fixture.descriptors.expected?.pagination?.offset;
207
+ if (expected !== void 0) expect(result).toEqual(expected);
208
+ observed(fixture, {
209
+ lifecycle: "query",
210
+ kind: "list",
211
+ descriptor
212
+ }, controller.signal);
213
+ });
214
+ it("preserves cursor pagination, and pages with the cursor it minted", async () => {
215
+ const provider = await fixture.create();
216
+ const descriptor = fixture.descriptors.pagination?.cursor;
217
+ cursorDeclarationRequired(provider.queries?.list !== void 0, fixture.descriptors);
218
+ if (descriptor === void 0 || provider.queries?.list === void 0) return;
219
+ const controller = new AbortController();
220
+ const result = await provider.queries.list(descriptor, context(controller.signal));
221
+ queryShape("list", result);
222
+ cursorPageShape(result);
223
+ const expected = fixture.descriptors.expected?.pagination?.cursor;
224
+ if (expected !== void 0) expect(result).toEqual(expected);
225
+ observed(fixture, {
226
+ lifecycle: "query",
227
+ kind: "list",
228
+ descriptor
229
+ }, controller.signal);
230
+ cursorPagesRequired(result);
231
+ const next = result.pageInfo?.nextCursor;
232
+ if (next === void 0) return;
233
+ const second = new AbortController();
234
+ const page2 = await provider.queries.list({
235
+ ...descriptor,
236
+ pagination: {
237
+ ...descriptor.pagination ?? {},
238
+ after: next
239
+ }
240
+ }, context(second.signal));
241
+ expect(page2.data, "a minted nextCursor promises a further page; the page it addresses must not be empty").not.toHaveLength(0);
242
+ expect(page2.data?.[0], "the page after a cursor must differ from the page that minted it — a provider ignoring `after` lands here").not.toEqual(result.data?.[0]);
243
+ });
244
+ it("opens live plans with the complete descriptor and subscribes with an AbortSignal", async () => {
245
+ const provider = await fixture.create();
246
+ const descriptor = fixture.descriptors.live;
247
+ liveDescriptorRequired(provider.live !== void 0, descriptor);
248
+ if (provider.live === void 0 || descriptor === void 0) return;
249
+ const openController = new AbortController();
250
+ const plan = await provider.live.open(descriptor, context(openController.signal));
251
+ observed(fixture, {
252
+ lifecycle: "live-open",
253
+ descriptor
254
+ }, openController.signal);
255
+ livePlan(plan);
256
+ if (plan === null) return;
257
+ const subscribeController = new AbortController();
258
+ const unsubscribe = await plan.subscribe({
259
+ next: () => void 0,
260
+ status: () => void 0,
261
+ error: () => void 0,
262
+ complete: () => void 0
263
+ }, { signal: subscribeController.signal });
264
+ observed(fixture, { lifecycle: "live-subscribe" }, subscribeController.signal);
265
+ await unsubscribe();
266
+ });
267
+ });
268
+ }
269
+ //#endregion
270
+ export { dataProviderContract };
271
+
272
+ //# sourceMappingURL=provider-contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-contract.js","names":[],"sources":["../src/data-provider-contract.ts"],"sourcesContent":["import {\n type BatchedQueryOperation,\n type DataError,\n type DataProvider,\n type ErasedMutationDescriptor,\n type ErasedQueryDescriptor,\n type LiveObserver,\n isDataError,\n resolveDataCapability,\n validateBatchResult,\n validateMutationResult,\n} from '@burojs/core';\nimport { describe, expect, it } from 'vitest';\n\ntype QueryKind = ErasedQueryDescriptor['kind'];\ntype MutationKind = ErasedMutationDescriptor['kind'];\n\nexport interface ProviderObservedCall {\n readonly lifecycle: 'query' | 'mutation' | 'batch' | 'live-open' | 'live-subscribe';\n readonly kind?: QueryKind | MutationKind;\n readonly descriptor?: ErasedQueryDescriptor | ErasedMutationDescriptor;\n readonly descriptors?: readonly ErasedQueryDescriptor[];\n readonly signal?: AbortSignal;\n}\n\n/** Specimens supplied by an adapter author; unsupported optional methods are omitted. */\nexport interface ProviderContractDescriptors {\n readonly queries?: Partial<Record<QueryKind, ErasedQueryDescriptor>>;\n readonly mutations?: Partial<Record<MutationKind, ErasedMutationDescriptor>>;\n readonly batch?: readonly BatchedQueryOperation[];\n /** Independent batch groups prove that grouping is explicit, never merged by the helper. */\n readonly batchGroups?: readonly (readonly BatchedQueryOperation[])[];\n /** List specimens for both pagination protocols, including expected totals/pageInfo. */\n readonly pagination?: {\n readonly offset?: ErasedQueryDescriptor;\n readonly cursor?: ErasedQueryDescriptor;\n };\n readonly live?: ErasedQueryDescriptor;\n /** Error specimens must make the selected handler reject with a DataError. */\n readonly errors?: readonly ProviderContractErrorSpecimen[];\n /** Hand-authored mapped results; assertions never derive expectations from an adapter. */\n readonly expected?: {\n readonly queries?: Partial<Record<QueryKind, unknown>>;\n readonly mutations?: Partial<Record<MutationKind, unknown>>;\n readonly batch?: unknown;\n readonly pagination?: { readonly offset?: unknown; readonly cursor?: unknown };\n };\n}\n\nexport type ProviderContractErrorSpecimen =\n | {\n readonly lifecycle: 'query';\n readonly kind: QueryKind;\n readonly descriptor: ErasedQueryDescriptor;\n }\n | {\n readonly lifecycle: 'mutation';\n readonly kind: MutationKind;\n readonly descriptor: ErasedMutationDescriptor;\n };\n\nexport interface ProviderContractFixture {\n create(): DataProvider | Promise<DataProvider>;\n descriptors: ProviderContractDescriptors;\n observeCalls(): readonly ProviderObservedCall[];\n}\n\nconst SESSION = Object.freeze({\n sessionKey: 'contract-session',\n scopeKey: 'contract-scope',\n generation: 1,\n});\nconst REQUEST_CONTEXT = Object.freeze({ contract: 'provider' });\n\nfunction context(signal: AbortSignal) {\n return Object.freeze({\n signal,\n requestContext: REQUEST_CONTEXT,\n session: SESSION,\n providerServiceKey: 'contract-provider',\n });\n}\n\nfunction queryShape(kind: QueryKind, value: unknown): void {\n if (kind === 'custom') return;\n expect(value).toBeTypeOf('object');\n if (kind === 'list' || kind === 'many')\n expect((value as { data?: unknown }).data).toBeInstanceOf(Array);\n if (kind === 'one') expect(value).toHaveProperty('data');\n if (kind === 'count')\n expect([null, 'number']).toContain(typeof (value as { total?: unknown }).total);\n}\n\nfunction observed(\n fixture: ProviderContractFixture,\n expected: Omit<ProviderObservedCall, 'signal'>,\n signal: AbortSignal,\n): void {\n expect(fixture.observeCalls()).toContainEqual(expect.objectContaining({ ...expected, signal }));\n}\n\nasync function assertBatch(\n provider: DataProvider,\n fixture: ProviderContractFixture,\n operations: readonly BatchedQueryOperation[],\n expected: unknown,\n): Promise<void> {\n expect(provider.batch?.queries).toBeTypeOf('function');\n const controller = new AbortController();\n const result = await provider.batch!.queries(operations, context(controller.signal));\n validateBatchResult(operations, result);\n if (expected !== undefined) expect(result).toEqual(expected);\n for (const entry of result.results) if (entry.status === 'error') dataError(entry.error);\n observed(\n fixture,\n { lifecycle: 'batch', descriptors: operations.map((operation) => operation.descriptor) },\n controller.signal,\n );\n}\n\nfunction dataError(value: unknown): asserts value is DataError {\n expect(isDataError(value)).toBe(true);\n}\n\n/**\n * A supplied `descriptors.live` IS the adapter author's declaration that this\n * descriptor should produce a plan — an adapter with no live support omits the\n * descriptor (or the `live` slot) and is skipped instead. So a null plan here\n * is a failure, not a skip.\n *\n * Split out as its own assertion, rather than left inline, so the suite's own\n * tests can prove it rejects — the same reason `queryShape`/`observed`/`batch`\n * already are.\n */\nfunction livePlan(value: unknown): void {\n expect(value, 'descriptors.live was supplied, so live.open must return a plan').not.toBeNull();\n expect(value).toBeTypeOf('object');\n expect((value as { subscribe?: unknown } | null)?.subscribe).toBeTypeOf('function');\n}\n\n/**\n * The OTHER door into the same free green. `livePlan` closes \"supplied a\n * descriptor, returned a null plan\"; this closes \"provider has a `live` slot,\n * fixture supplied no descriptor\" — which used to `return` silently, leaving\n * the live cell green with nothing executed. Deleting the single line\n * `live: listDescriptor` from a fixture left `examples/marketplace-ops`'s\n * hasura contract at 35/35 green with the live `it` still passing.\n *\n * The rule is asymmetric on purpose: a provider with NO `live` slot is a\n * provider without live support, and skipping is right. A provider that HAS\n * one and a fixture that says nothing about it is an untested capability\n * reported as a pass.\n *\n * Split out for the same reason `livePlan` is: the suite's own tests drive it\n * directly to prove it rejects.\n */\nfunction liveDescriptorRequired(hasLiveSlot: boolean, descriptor: unknown): void {\n if (!hasLiveSlot) return;\n expect(\n descriptor,\n 'the provider exposes a `live` slot, so the fixture must supply `descriptors.live` — omitting it silently skips the whole live cell',\n ).toBeDefined();\n}\n\n/**\n * The THIRD door into a free green, and the one this suite carried longest:\n * the cursor pagination cell.\n *\n * It used to be `if (descriptor === undefined || ...) return;` — so a fixture\n * that simply said nothing about cursors got a pass for a cell titled\n * \"preserves cursor pagination\". MEASURED, not argued: with\n * `@burojs/hasura`'s keyset implementation sabotaged so that the keyset\n * predicate was dropped entirely — every cursor page returning page one,\n * forever — `examples/marketplace-ops/tests/hasura-provider-contract.test.ts`\n * stayed 35/35 green, cursor cell included.\n *\n * Unlike `live`, cursor support cannot be read off the provider object: there\n * is no `queries.listCursor` slot, only `queries.list` taking a pagination\n * shape it may or may not serve. So the rule is a DECLARATION rule instead —\n * a fixture whose provider lists must SAY which of the two it is:\n *\n * - `descriptors.pagination.cursor` — \"this provider serves cursor pages\",\n * and the cell below then holds it to that (`pageInfo`, and a second page\n * that is really a second page); or\n * - an `errors` specimen carrying a cursor list descriptor — \"this provider\n * refuses cursor pagination\", proven by execution.\n *\n * Silence is the one thing that is no longer allowed. Both demo fixtures\n * already chose the second form deliberately, which is exactly why the\n * vacuity survived so long: the suite looked adequately used.\n */\nfunction cursorDeclarationRequired(\n hasListSlot: boolean,\n descriptors: ProviderContractDescriptors,\n): void {\n if (!hasListSlot) return;\n const refusalSpecimen = (descriptors.errors ?? []).some(\n (specimen) =>\n specimen.lifecycle === 'query' &&\n specimen.kind === 'list' &&\n (specimen.descriptor as { pagination?: { kind?: unknown } }).pagination?.kind === 'cursor',\n );\n expect(\n descriptors.pagination?.cursor !== undefined || refusalSpecimen,\n 'the provider exposes a `list` slot, so the fixture must state its cursor-pagination position: supply `descriptors.pagination.cursor` for a provider that serves cursor pages, or an `errors` specimen holding a cursor list descriptor for one that refuses. Saying nothing used to pass this cell for a provider whose cursor paging was entirely broken.',\n ).toBe(true);\n}\n\n/**\n * What a cursor page must carry, beyond the `data` array every list shares.\n *\n * `pageInfo` is what makes a cursor list navigable at all — `nextCursor` is\n * the only thing a caller can hand back, and the framework's infinite-list\n * observer stops paging the moment it is absent. A provider that answered a\n * cursor descriptor with a bare `{data}` would render a single page and look\n * finished, which the old cell accepted.\n *\n * (Named without its package: this module is asserted to import nothing but\n * core contracts, and `tests/package.test.ts` greps the source for the\n * forbidden names.)\n */\nfunction cursorPageShape(value: unknown): void {\n const pageInfo = (value as { pageInfo?: { nextCursor?: unknown; hasNextPage?: unknown } })\n .pageInfo;\n expect(\n pageInfo,\n 'a cursor list must return `pageInfo` — without it there is nothing to page with',\n ).toBeTypeOf('object');\n const next = pageInfo?.nextCursor;\n if (next !== undefined) expect(next).toBeTypeOf('string');\n const has = pageInfo?.hasNextPage;\n if (has !== undefined) expect(has).toBeTypeOf('boolean');\n}\n\n/**\n * The first page of a supplied cursor descriptor must really BE a first page —\n * it has to mint a `nextCursor` the cell can then page with.\n *\n * The residual free-green door this closes (found by sabotage at the SP4 final\n * review, and reproducible: hard-code `hasNextPage = false` in\n * `@burojs/hasura`'s cursor branch). `cursorPageShape` only type-checks\n * `nextCursor`/`hasNextPage` WHEN PRESENT, and the cell's second-page half then\n * read `if (next === undefined) return;`. So a provider that minted no cursor\n * on page one skipped the entire \"the second page really differs\" half and\n * passed a cell named \"preserves cursor pagination\" — the same shape of vacuity\n * SP4 task 3 removed from this cell's outer skip, one level in.\n *\n * The rule is the same one `livePlan` applies to `descriptors.live`: supplying\n * `descriptors.pagination.cursor` is the AUTHOR'S DECLARATION that this\n * descriptor pages, so a provider answering it with a finished list is a\n * failure, not a skip. It is satisfied by sizing the descriptor's `limit` below\n * the fixture's row count — the fixture author controls both. A provider that\n * genuinely cannot mint a cursor does not serve cursor pages: declare the\n * `errors` refusal specimen instead, which `cursorDeclarationRequired` already\n * accepts as the other honest position.\n */\nfunction cursorPagesRequired(value: unknown): void {\n const pageInfo = (value as { pageInfo?: { nextCursor?: unknown; hasNextPage?: unknown } })\n .pageInfo;\n expect(\n pageInfo?.hasNextPage,\n 'a supplied `descriptors.pagination.cursor` declares that this descriptor PAGES, so its first page must report `hasNextPage: true`. Size the descriptor `limit` below the fixture row count; a provider that cannot page belongs in an `errors` refusal specimen instead. (A first page that reports itself finished used to skip the whole second-page half of this cell.)',\n ).toBe(true);\n const next = pageInfo?.nextCursor;\n expect(\n typeof next === 'string' && next.length > 0,\n 'a first page reporting `hasNextPage: true` must also mint a non-empty `nextCursor` — it is the only thing a caller can hand back, and without it the infinite-list observer stops paging',\n ).toBe(true);\n}\n\n/** @internal Direct assertion seam for the contract suite's intentionally broken-provider tests. */\nexport const __providerContractAssertions = Object.freeze({\n queryShape,\n observed,\n batch: validateBatchResult,\n mutation: validateMutationResult,\n dataError,\n livePlan,\n liveDescriptorRequired,\n cursorDeclarationRequired,\n cursorPageShape,\n cursorPagesRequired,\n});\n\n/**\n * Registers transport-agnostic provider conformance tests. It intentionally\n * imports only core contracts: adapters supply their own deterministic client\n * and expose provider-boundary observations through the fixture.\n */\nexport function dataProviderContract(name: string, fixture: ProviderContractFixture): void {\n describe(`${name} data provider contract`, () => {\n for (const kind of ['list', 'one', 'many', 'count', 'custom'] as const) {\n it(`preserves the ${kind} query descriptor and AbortSignal`, async () => {\n const provider = await fixture.create();\n const descriptor = fixture.descriptors.queries?.[kind];\n const handler = provider.queries?.[kind];\n if (descriptor === undefined || handler === undefined) return;\n const controller = new AbortController();\n const result = await handler(descriptor as never, context(controller.signal));\n queryShape(kind, result);\n const expected = fixture.descriptors.expected?.queries?.[kind];\n if (expected !== undefined) expect(result).toEqual(expected);\n observed(fixture, { lifecycle: 'query', kind, descriptor }, controller.signal);\n });\n }\n\n for (const kind of [\n 'createOne',\n 'createMany',\n 'updateOne',\n 'updateMany',\n 'deleteOne',\n 'deleteMany',\n 'custom',\n ] as const) {\n it(`validates ${kind} mutation output and preserves its request`, async () => {\n const provider = await fixture.create();\n const descriptor = fixture.descriptors.mutations?.[kind];\n const handler = provider.mutations?.[kind];\n if (descriptor === undefined || handler === undefined) return;\n const controller = new AbortController();\n const result = await handler(descriptor as never, context(controller.signal));\n validateMutationResult(descriptor, result);\n const expected = fixture.descriptors.expected?.mutations?.[kind];\n if (expected !== undefined) expect(result).toEqual(expected);\n observed(fixture, { lifecycle: 'mutation', kind, descriptor }, controller.signal);\n });\n }\n\n for (const specimen of fixture.descriptors.errors ?? []) {\n it(`returns a structured error for the ${specimen.lifecycle}.${specimen.kind} error specimen`, async () => {\n const provider = await fixture.create();\n const controller = new AbortController();\n const handler =\n specimen.lifecycle === 'query'\n ? provider.queries?.[specimen.kind as QueryKind]\n : provider.mutations?.[specimen.kind as MutationKind];\n expect(handler).toBeTypeOf('function');\n try {\n await (\n handler as (\n descriptor: never,\n operation: ReturnType<typeof context>,\n ) => Promise<unknown>\n )(specimen.descriptor as never, context(controller.signal));\n throw new Error('Provider error specimen resolved instead of rejecting.');\n } catch (error) {\n dataError(error);\n }\n observed(\n fixture,\n { lifecycle: specimen.lifecycle, kind: specimen.kind, descriptor: specimen.descriptor },\n controller.signal,\n );\n });\n }\n\n it('reports absent optional capabilities without invoking fallback handlers', async () => {\n const provider = await fixture.create();\n const before = fixture.observeCalls().length;\n for (const kind of ['list', 'one', 'many', 'count', 'custom'] as const) {\n if (provider.queries?.[kind] === undefined) {\n const capability = resolveDataCapability(provider, { lifecycle: 'query', kind });\n expect(capability).toMatchObject({ kind: 'capability', code: 'missing-data-capability' });\n }\n }\n for (const kind of [\n 'createOne',\n 'createMany',\n 'updateOne',\n 'updateMany',\n 'deleteOne',\n 'deleteMany',\n 'custom',\n ] as const) {\n if (provider.mutations?.[kind] === undefined) {\n const capability = resolveDataCapability(provider, { lifecycle: 'mutation', kind });\n expect(capability).toMatchObject({ kind: 'capability', code: 'missing-data-capability' });\n }\n }\n expect(fixture.observeCalls()).toHaveLength(before);\n });\n\n it('validates batch IDs, kinds, partial errors, and the shared AbortSignal', async () => {\n const provider = await fixture.create();\n const operations = fixture.descriptors.batch;\n if (provider.batch?.queries === undefined || operations === undefined) return;\n await assertBatch(provider, fixture, operations, fixture.descriptors.expected?.batch);\n });\n\n for (const [index, operations] of (fixture.descriptors.batchGroups ?? []).entries()) {\n it(`keeps batch group ${index + 1} isolated and validates its partial results`, async () => {\n const provider = await fixture.create();\n await assertBatch(provider, fixture, operations, undefined);\n });\n }\n\n it('preserves offset pagination and its mapped total/page shape', async () => {\n const provider = await fixture.create();\n const descriptor = fixture.descriptors.pagination?.offset;\n if (descriptor === undefined || provider.queries?.list === undefined) return;\n const controller = new AbortController();\n const result = await provider.queries.list(descriptor as never, context(controller.signal));\n queryShape('list', result);\n const expected = fixture.descriptors.expected?.pagination?.offset;\n if (expected !== undefined) expect(result).toEqual(expected);\n observed(fixture, { lifecycle: 'query', kind: 'list', descriptor }, controller.signal);\n });\n\n it('preserves cursor pagination, and pages with the cursor it minted', async () => {\n const provider = await fixture.create();\n const descriptor = fixture.descriptors.pagination?.cursor;\n // A fixture that says NOTHING about cursors fails here rather than\n // skipping — see `cursorDeclarationRequired`.\n cursorDeclarationRequired(provider.queries?.list !== undefined, fixture.descriptors);\n if (descriptor === undefined || provider.queries?.list === undefined) return;\n const controller = new AbortController();\n const result = await provider.queries.list(descriptor as never, context(controller.signal));\n queryShape('list', result);\n cursorPageShape(result);\n const expected = fixture.descriptors.expected?.pagination?.cursor;\n if (expected !== undefined) expect(result).toEqual(expected);\n observed(fixture, { lifecycle: 'query', kind: 'list', descriptor }, controller.signal);\n\n // The half a one-page assertion cannot reach: a provider that ACCEPTS\n // `after` and ignores it returns page one again, forever, and every\n // shape check above still passes. The second page is therefore fetched\n // and required to be a different, non-empty page.\n //\n // This used to read `if (next === undefined) return;`, which handed the\n // whole half back as a free green to any provider that minted no cursor\n // — see `cursorPagesRequired`, which is now what stands here.\n cursorPagesRequired(result);\n const next = (result as { pageInfo?: { nextCursor?: string } }).pageInfo?.nextCursor;\n if (next === undefined) return; // unreachable past `cursorPagesRequired`; narrows the type\n const second = new AbortController();\n const page2 = (await provider.queries.list(\n {\n ...(descriptor as unknown as Record<string, unknown>),\n pagination: {\n ...((descriptor as unknown as { pagination?: Record<string, unknown> }).pagination ??\n {}),\n after: next,\n },\n } as never,\n context(second.signal),\n )) as { data?: readonly unknown[] };\n expect(\n page2.data,\n 'a minted nextCursor promises a further page; the page it addresses must not be empty',\n ).not.toHaveLength(0);\n expect(\n page2.data?.[0],\n 'the page after a cursor must differ from the page that minted it — a provider ignoring `after` lands here',\n ).not.toEqual((result as { data?: readonly unknown[] }).data?.[0]);\n });\n\n it('opens live plans with the complete descriptor and subscribes with an AbortSignal', async () => {\n const provider = await fixture.create();\n const descriptor = fixture.descriptors.live;\n liveDescriptorRequired(provider.live !== undefined, descriptor);\n if (provider.live === undefined || descriptor === undefined) return;\n const openController = new AbortController();\n const plan = await provider.live.open(descriptor, context(openController.signal));\n observed(fixture, { lifecycle: 'live-open', descriptor }, openController.signal);\n // A null plan used to `return` here, which handed every adapter a free\n // green on the whole live cell: `subscribe` was never reached, so\n // nothing about the plan was ever checked, and the `it` passed for an\n // adapter that had done no live work at all.\n livePlan(plan);\n if (plan === null) return; // unreachable past `livePlan`; narrows the type\n const subscribeController = new AbortController();\n const observer: LiveObserver = {\n next: () => undefined,\n status: () => undefined,\n error: () => undefined,\n complete: () => undefined,\n };\n const unsubscribe = await plan.subscribe(observer, { signal: subscribeController.signal });\n observed(fixture, { lifecycle: 'live-subscribe' }, subscribeController.signal);\n await unsubscribe();\n });\n });\n}\n"],"mappings":";;;AAmEA,MAAM,UAAU,OAAO,OAAO;CAC5B,YAAY;CACZ,UAAU;CACV,YAAY;AACd,CAAC;AACD,MAAM,kBAAkB,OAAO,OAAO,EAAE,UAAU,WAAW,CAAC;AAE9D,SAAS,QAAQ,QAAqB;CACpC,OAAO,OAAO,OAAO;EACnB;EACA,gBAAgB;EAChB,SAAS;EACT,oBAAoB;CACtB,CAAC;AACH;AAEA,SAAS,WAAW,MAAiB,OAAsB;CACzD,IAAI,SAAS,UAAU;CACvB,OAAO,KAAK,CAAC,CAAC,WAAW,QAAQ;CACjC,IAAI,SAAS,UAAU,SAAS,QAC9B,OAAQ,MAA6B,IAAI,CAAC,CAAC,eAAe,KAAK;CACjE,IAAI,SAAS,OAAO,OAAO,KAAK,CAAC,CAAC,eAAe,MAAM;CACvD,IAAI,SAAS,SACX,OAAO,CAAC,MAAM,QAAQ,CAAC,CAAC,CAAC,UAAU,OAAQ,MAA8B,KAAK;AAClF;AAEA,SAAS,SACP,SACA,UACA,QACM;CACN,OAAO,QAAQ,aAAa,CAAC,CAAC,CAAC,eAAe,OAAO,iBAAiB;EAAE,GAAG;EAAU;CAAO,CAAC,CAAC;AAChG;AAEA,eAAe,YACb,UACA,SACA,YACA,UACe;CACf,OAAO,SAAS,OAAO,OAAO,CAAC,CAAC,WAAW,UAAU;CACrD,MAAM,aAAa,IAAI,gBAAgB;CACvC,MAAM,SAAS,MAAM,SAAS,MAAO,QAAQ,YAAY,QAAQ,WAAW,MAAM,CAAC;CACnF,oBAAoB,YAAY,MAAM;CACtC,IAAI,aAAa,KAAA,GAAW,OAAO,MAAM,CAAC,CAAC,QAAQ,QAAQ;CAC3D,KAAK,MAAM,SAAS,OAAO,SAAS,IAAI,MAAM,WAAW,SAAS,UAAU,MAAM,KAAK;CACvF,SACE,SACA;EAAE,WAAW;EAAS,aAAa,WAAW,KAAK,cAAc,UAAU,UAAU;CAAE,GACvF,WAAW,MACb;AACF;AAEA,SAAS,UAAU,OAA4C;CAC7D,OAAO,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI;AACtC;AAYA,SAAS,SAAS,OAAsB;CACtC,OAAO,OAAO,gEAAgE,CAAC,CAAC,IAAI,SAAS;CAC7F,OAAO,KAAK,CAAC,CAAC,WAAW,QAAQ;CACjC,OAAQ,OAA0C,SAAS,CAAC,CAAC,WAAW,UAAU;AACpF;AAkBA,SAAS,uBAAuB,aAAsB,YAA2B;CAC/E,IAAI,CAAC,aAAa;CAClB,OACE,YACA,oIACF,CAAC,CAAC,YAAY;AAChB;AA6BA,SAAS,0BACP,aACA,aACM;CACN,IAAI,CAAC,aAAa;CAClB,MAAM,mBAAmB,YAAY,UAAU,CAAC,EAAA,CAAG,MAChD,aACC,SAAS,cAAc,WACvB,SAAS,SAAS,UACjB,SAAS,WAAmD,YAAY,SAAS,QACtF;CACA,OACE,YAAY,YAAY,WAAW,KAAA,KAAa,iBAChD,4VACF,CAAC,CAAC,KAAK,IAAI;AACb;AAeA,SAAS,gBAAgB,OAAsB;CAC7C,MAAM,WAAY,MACf;CACH,OACE,UACA,iFACF,CAAC,CAAC,WAAW,QAAQ;CACrB,MAAM,OAAO,UAAU;CACvB,IAAI,SAAS,KAAA,GAAW,OAAO,IAAI,CAAC,CAAC,WAAW,QAAQ;CACxD,MAAM,MAAM,UAAU;CACtB,IAAI,QAAQ,KAAA,GAAW,OAAO,GAAG,CAAC,CAAC,WAAW,SAAS;AACzD;AAwBA,SAAS,oBAAoB,OAAsB;CACjD,MAAM,WAAY,MACf;CACH,OACE,UAAU,aACV,4WACF,CAAC,CAAC,KAAK,IAAI;CACX,MAAM,OAAO,UAAU;CACvB,OACE,OAAO,SAAS,YAAY,KAAK,SAAS,GAC1C,0LACF,CAAC,CAAC,KAAK,IAAI;AACb;AAG4C,OAAO,OAAO;CACxD;CACA;CACA,OAAO;CACP,UAAU;CACV;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAOD,SAAgB,qBAAqB,MAAc,SAAwC;CACzF,SAAS,GAAG,KAAK,gCAAgC;EAC/C,KAAK,MAAM,QAAQ;GAAC;GAAQ;GAAO;GAAQ;GAAS;EAAQ,GAC1D,GAAG,iBAAiB,KAAK,oCAAoC,YAAY;GACvE,MAAM,WAAW,MAAM,QAAQ,OAAO;GACtC,MAAM,aAAa,QAAQ,YAAY,UAAU;GACjD,MAAM,UAAU,SAAS,UAAU;GACnC,IAAI,eAAe,KAAA,KAAa,YAAY,KAAA,GAAW;GACvD,MAAM,aAAa,IAAI,gBAAgB;GACvC,MAAM,SAAS,MAAM,QAAQ,YAAqB,QAAQ,WAAW,MAAM,CAAC;GAC5E,WAAW,MAAM,MAAM;GACvB,MAAM,WAAW,QAAQ,YAAY,UAAU,UAAU;GACzD,IAAI,aAAa,KAAA,GAAW,OAAO,MAAM,CAAC,CAAC,QAAQ,QAAQ;GAC3D,SAAS,SAAS;IAAE,WAAW;IAAS;IAAM;GAAW,GAAG,WAAW,MAAM;EAC/E,CAAC;EAGH,KAAK,MAAM,QAAQ;GACjB;GACA;GACA;GACA;GACA;GACA;GACA;EACF,GACE,GAAG,aAAa,KAAK,6CAA6C,YAAY;GAC5E,MAAM,WAAW,MAAM,QAAQ,OAAO;GACtC,MAAM,aAAa,QAAQ,YAAY,YAAY;GACnD,MAAM,UAAU,SAAS,YAAY;GACrC,IAAI,eAAe,KAAA,KAAa,YAAY,KAAA,GAAW;GACvD,MAAM,aAAa,IAAI,gBAAgB;GACvC,MAAM,SAAS,MAAM,QAAQ,YAAqB,QAAQ,WAAW,MAAM,CAAC;GAC5E,uBAAuB,YAAY,MAAM;GACzC,MAAM,WAAW,QAAQ,YAAY,UAAU,YAAY;GAC3D,IAAI,aAAa,KAAA,GAAW,OAAO,MAAM,CAAC,CAAC,QAAQ,QAAQ;GAC3D,SAAS,SAAS;IAAE,WAAW;IAAY;IAAM;GAAW,GAAG,WAAW,MAAM;EAClF,CAAC;EAGH,KAAK,MAAM,YAAY,QAAQ,YAAY,UAAU,CAAC,GACpD,GAAG,sCAAsC,SAAS,UAAU,GAAG,SAAS,KAAK,kBAAkB,YAAY;GACzG,MAAM,WAAW,MAAM,QAAQ,OAAO;GACtC,MAAM,aAAa,IAAI,gBAAgB;GACvC,MAAM,UACJ,SAAS,cAAc,UACnB,SAAS,UAAU,SAAS,QAC5B,SAAS,YAAY,SAAS;GACpC,OAAO,OAAO,CAAC,CAAC,WAAW,UAAU;GACrC,IAAI;IACF,MACE,QAIA,SAAS,YAAqB,QAAQ,WAAW,MAAM,CAAC;IAC1D,MAAM,IAAI,MAAM,wDAAwD;GAC1E,SAAS,OAAO;IACd,UAAU,KAAK;GACjB;GACA,SACE,SACA;IAAE,WAAW,SAAS;IAAW,MAAM,SAAS;IAAM,YAAY,SAAS;GAAW,GACtF,WAAW,MACb;EACF,CAAC;EAGH,GAAG,2EAA2E,YAAY;GACxF,MAAM,WAAW,MAAM,QAAQ,OAAO;GACtC,MAAM,SAAS,QAAQ,aAAa,CAAC,CAAC;GACtC,KAAK,MAAM,QAAQ;IAAC;IAAQ;IAAO;IAAQ;IAAS;GAAQ,GAC1D,IAAI,SAAS,UAAU,UAAU,KAAA,GAAW;IAC1C,MAAM,aAAa,sBAAsB,UAAU;KAAE,WAAW;KAAS;IAAK,CAAC;IAC/E,OAAO,UAAU,CAAC,CAAC,cAAc;KAAE,MAAM;KAAc,MAAM;IAA0B,CAAC;GAC1F;GAEF,KAAK,MAAM,QAAQ;IACjB;IACA;IACA;IACA;IACA;IACA;IACA;GACF,GACE,IAAI,SAAS,YAAY,UAAU,KAAA,GAAW;IAC5C,MAAM,aAAa,sBAAsB,UAAU;KAAE,WAAW;KAAY;IAAK,CAAC;IAClF,OAAO,UAAU,CAAC,CAAC,cAAc;KAAE,MAAM;KAAc,MAAM;IAA0B,CAAC;GAC1F;GAEF,OAAO,QAAQ,aAAa,CAAC,CAAC,CAAC,aAAa,MAAM;EACpD,CAAC;EAED,GAAG,0EAA0E,YAAY;GACvF,MAAM,WAAW,MAAM,QAAQ,OAAO;GACtC,MAAM,aAAa,QAAQ,YAAY;GACvC,IAAI,SAAS,OAAO,YAAY,KAAA,KAAa,eAAe,KAAA,GAAW;GACvE,MAAM,YAAY,UAAU,SAAS,YAAY,QAAQ,YAAY,UAAU,KAAK;EACtF,CAAC;EAED,KAAK,MAAM,CAAC,OAAO,gBAAgB,QAAQ,YAAY,eAAe,CAAC,EAAA,CAAG,QAAQ,GAChF,GAAG,qBAAqB,QAAQ,EAAE,8CAA8C,YAAY;GAE1F,MAAM,YAAY,MADK,QAAQ,OAAO,GACV,SAAS,YAAY,KAAA,CAAS;EAC5D,CAAC;EAGH,GAAG,+DAA+D,YAAY;GAC5E,MAAM,WAAW,MAAM,QAAQ,OAAO;GACtC,MAAM,aAAa,QAAQ,YAAY,YAAY;GACnD,IAAI,eAAe,KAAA,KAAa,SAAS,SAAS,SAAS,KAAA,GAAW;GACtE,MAAM,aAAa,IAAI,gBAAgB;GACvC,MAAM,SAAS,MAAM,SAAS,QAAQ,KAAK,YAAqB,QAAQ,WAAW,MAAM,CAAC;GAC1F,WAAW,QAAQ,MAAM;GACzB,MAAM,WAAW,QAAQ,YAAY,UAAU,YAAY;GAC3D,IAAI,aAAa,KAAA,GAAW,OAAO,MAAM,CAAC,CAAC,QAAQ,QAAQ;GAC3D,SAAS,SAAS;IAAE,WAAW;IAAS,MAAM;IAAQ;GAAW,GAAG,WAAW,MAAM;EACvF,CAAC;EAED,GAAG,oEAAoE,YAAY;GACjF,MAAM,WAAW,MAAM,QAAQ,OAAO;GACtC,MAAM,aAAa,QAAQ,YAAY,YAAY;GAGnD,0BAA0B,SAAS,SAAS,SAAS,KAAA,GAAW,QAAQ,WAAW;GACnF,IAAI,eAAe,KAAA,KAAa,SAAS,SAAS,SAAS,KAAA,GAAW;GACtE,MAAM,aAAa,IAAI,gBAAgB;GACvC,MAAM,SAAS,MAAM,SAAS,QAAQ,KAAK,YAAqB,QAAQ,WAAW,MAAM,CAAC;GAC1F,WAAW,QAAQ,MAAM;GACzB,gBAAgB,MAAM;GACtB,MAAM,WAAW,QAAQ,YAAY,UAAU,YAAY;GAC3D,IAAI,aAAa,KAAA,GAAW,OAAO,MAAM,CAAC,CAAC,QAAQ,QAAQ;GAC3D,SAAS,SAAS;IAAE,WAAW;IAAS,MAAM;IAAQ;GAAW,GAAG,WAAW,MAAM;GAUrF,oBAAoB,MAAM;GAC1B,MAAM,OAAQ,OAAkD,UAAU;GAC1E,IAAI,SAAS,KAAA,GAAW;GACxB,MAAM,SAAS,IAAI,gBAAgB;GACnC,MAAM,QAAS,MAAM,SAAS,QAAQ,KACpC;IACE,GAAI;IACJ,YAAY;KACV,GAAK,WAAmE,cACtE,CAAC;KACH,OAAO;IACT;GACF,GACA,QAAQ,OAAO,MAAM,CACvB;GACA,OACE,MAAM,MACN,sFACF,CAAC,CAAC,IAAI,aAAa,CAAC;GACpB,OACE,MAAM,OAAO,IACb,2GACF,CAAC,CAAC,IAAI,QAAS,OAAyC,OAAO,EAAE;EACnE,CAAC;EAED,GAAG,oFAAoF,YAAY;GACjG,MAAM,WAAW,MAAM,QAAQ,OAAO;GACtC,MAAM,aAAa,QAAQ,YAAY;GACvC,uBAAuB,SAAS,SAAS,KAAA,GAAW,UAAU;GAC9D,IAAI,SAAS,SAAS,KAAA,KAAa,eAAe,KAAA,GAAW;GAC7D,MAAM,iBAAiB,IAAI,gBAAgB;GAC3C,MAAM,OAAO,MAAM,SAAS,KAAK,KAAK,YAAY,QAAQ,eAAe,MAAM,CAAC;GAChF,SAAS,SAAS;IAAE,WAAW;IAAa;GAAW,GAAG,eAAe,MAAM;GAK/E,SAAS,IAAI;GACb,IAAI,SAAS,MAAM;GACnB,MAAM,sBAAsB,IAAI,gBAAgB;GAOhD,MAAM,cAAc,MAAM,KAAK,UAAU;IALvC,YAAY,KAAA;IACZ,cAAc,KAAA;IACd,aAAa,KAAA;IACb,gBAAgB,KAAA;GAE8B,GAAG,EAAE,QAAQ,oBAAoB,OAAO,CAAC;GACzF,SAAS,SAAS,EAAE,WAAW,iBAAiB,GAAG,oBAAoB,MAAM;GAC7E,MAAM,YAAY;EACpB,CAAC;CACH,CAAC;AACH"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burojs/test-helpers",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "license": "MIT",
5
5
  "description": "Buro: typed Playwright Page Objects over the testid convention",
6
6
  "type": "module",
@@ -18,23 +18,39 @@
18
18
  "default": "./dist/index.cjs"
19
19
  }
20
20
  },
21
+ "./provider-contract": {
22
+ "import": {
23
+ "types": "./dist/provider-contract.d.ts",
24
+ "default": "./dist/provider-contract.js"
25
+ },
26
+ "require": {
27
+ "types": "./dist/provider-contract.d.cts",
28
+ "default": "./dist/provider-contract.cjs"
29
+ }
30
+ },
21
31
  "./package.json": "./package.json"
22
32
  },
33
+ "typesVersions": {
34
+ "*": {
35
+ "provider-contract": [
36
+ "./dist/provider-contract.d.ts"
37
+ ]
38
+ }
39
+ },
23
40
  "files": [
24
41
  "dist",
25
42
  "README.md"
26
43
  ],
27
44
  "dependencies": {
28
- "@burojs/core": "0.2.0"
45
+ "@burojs/core": "0.4.0"
29
46
  },
30
47
  "peerDependencies": {
31
48
  "@playwright/test": "^1.49.0",
32
- "vitest": "^2.1.0"
49
+ "vitest": "^2.1.0 || ^3.0.0 || ^4.0.0"
33
50
  },
34
51
  "devDependencies": {
35
- "tsup": "^8.3.0",
36
52
  "typescript": "^5.5.0",
37
- "vitest": "^2.1.0",
53
+ "vitest": "^4.0.0",
38
54
  "expect-type": "^1.1.0",
39
55
  "@burojs/tooling-tsconfig": "0.0.0",
40
56
  "@burojs/tooling-eslint": "0.0.0"
@@ -43,8 +59,8 @@
43
59
  "access": "public"
44
60
  },
45
61
  "scripts": {
46
- "build": "tsup",
47
- "dev": "tsup --watch",
62
+ "build": "tsdown",
63
+ "dev": "tsdown --watch",
48
64
  "typecheck": "tsc --noEmit",
49
65
  "typecheck:tests": "tsc -p tsconfig.test.json"
50
66
  }