@tappify/extension-sdk 2.0.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.
Files changed (98) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunk-2DBSVKAU.cjs +65 -0
  3. package/dist/chunk-2DBSVKAU.cjs.map +1 -0
  4. package/dist/chunk-3YWVYA42.cjs +142 -0
  5. package/dist/chunk-3YWVYA42.cjs.map +1 -0
  6. package/dist/chunk-4C5Z3CIK.mjs +26 -0
  7. package/dist/chunk-4C5Z3CIK.mjs.map +1 -0
  8. package/dist/chunk-5AAPD5WV.cjs +250 -0
  9. package/dist/chunk-5AAPD5WV.cjs.map +1 -0
  10. package/dist/chunk-7KIKOH5H.cjs +26 -0
  11. package/dist/chunk-7KIKOH5H.cjs.map +1 -0
  12. package/dist/chunk-BDCX6LRQ.mjs +250 -0
  13. package/dist/chunk-BDCX6LRQ.mjs.map +1 -0
  14. package/dist/chunk-CSTYBKZC.mjs +65 -0
  15. package/dist/chunk-CSTYBKZC.mjs.map +1 -0
  16. package/dist/chunk-IGK2AP6J.mjs +142 -0
  17. package/dist/chunk-IGK2AP6J.mjs.map +1 -0
  18. package/dist/chunk-MNDVYPEP.mjs +302 -0
  19. package/dist/chunk-MNDVYPEP.mjs.map +1 -0
  20. package/dist/chunk-OPPYEA7F.cjs +33 -0
  21. package/dist/chunk-OPPYEA7F.cjs.map +1 -0
  22. package/dist/chunk-PYJMJF44.cjs +302 -0
  23. package/dist/chunk-PYJMJF44.cjs.map +1 -0
  24. package/dist/chunk-QHM7XNRK.mjs +33 -0
  25. package/dist/chunk-QHM7XNRK.mjs.map +1 -0
  26. package/dist/chunk-RMUWRESK.cjs +365 -0
  27. package/dist/chunk-RMUWRESK.cjs.map +1 -0
  28. package/dist/chunk-ZT35X7KL.mjs +365 -0
  29. package/dist/chunk-ZT35X7KL.mjs.map +1 -0
  30. package/dist/codegen.cjs +300 -0
  31. package/dist/codegen.cjs.map +1 -0
  32. package/dist/codegen.d.cts +432 -0
  33. package/dist/codegen.d.ts +432 -0
  34. package/dist/codegen.mjs +272 -0
  35. package/dist/codegen.mjs.map +1 -0
  36. package/dist/constants-DJ_KgZpN.d.cts +449 -0
  37. package/dist/constants-DJ_KgZpN.d.ts +449 -0
  38. package/dist/context-Dt6xh_5t.d.cts +30 -0
  39. package/dist/context-Ml7k_V14.d.ts +30 -0
  40. package/dist/errors-COsBh7kd.d.cts +88 -0
  41. package/dist/errors-COsBh7kd.d.ts +88 -0
  42. package/dist/handler-CZIlZ5vu.d.ts +227 -0
  43. package/dist/handler-kU9Jf36K.d.cts +227 -0
  44. package/dist/host.cjs +24 -0
  45. package/dist/host.cjs.map +1 -0
  46. package/dist/host.d.cts +24 -0
  47. package/dist/host.d.ts +24 -0
  48. package/dist/host.mjs +24 -0
  49. package/dist/host.mjs.map +1 -0
  50. package/dist/index.cjs +698 -0
  51. package/dist/index.cjs.map +1 -0
  52. package/dist/index.d.cts +1016 -0
  53. package/dist/index.d.ts +1016 -0
  54. package/dist/index.mjs +698 -0
  55. package/dist/index.mjs.map +1 -0
  56. package/dist/manifest.cjs +872 -0
  57. package/dist/manifest.cjs.map +1 -0
  58. package/dist/manifest.d.cts +432 -0
  59. package/dist/manifest.d.ts +432 -0
  60. package/dist/manifest.mjs +872 -0
  61. package/dist/manifest.mjs.map +1 -0
  62. package/dist/matchers-Cz93AK1n.d.cts +74 -0
  63. package/dist/matchers-Cz93AK1n.d.ts +74 -0
  64. package/dist/server.cjs +210 -0
  65. package/dist/server.cjs.map +1 -0
  66. package/dist/server.d.cts +243 -0
  67. package/dist/server.d.ts +243 -0
  68. package/dist/server.mjs +210 -0
  69. package/dist/server.mjs.map +1 -0
  70. package/dist/styles.css +367 -0
  71. package/dist/testing/index.cjs +887 -0
  72. package/dist/testing/index.cjs.map +1 -0
  73. package/dist/testing/index.d.cts +512 -0
  74. package/dist/testing/index.d.ts +512 -0
  75. package/dist/testing/index.mjs +887 -0
  76. package/dist/testing/index.mjs.map +1 -0
  77. package/dist/testing/jest.cjs +17 -0
  78. package/dist/testing/jest.cjs.map +1 -0
  79. package/dist/testing/jest.d.cts +12 -0
  80. package/dist/testing/jest.d.ts +12 -0
  81. package/dist/testing/jest.mjs +17 -0
  82. package/dist/testing/jest.mjs.map +1 -0
  83. package/dist/testing/vitest.cjs +15 -0
  84. package/dist/testing/vitest.cjs.map +1 -0
  85. package/dist/testing/vitest.d.cts +10 -0
  86. package/dist/testing/vitest.d.ts +10 -0
  87. package/dist/testing/vitest.mjs +15 -0
  88. package/dist/testing/vitest.mjs.map +1 -0
  89. package/dist/types-DDAGLpov.d.ts +715 -0
  90. package/dist/types-DrpQSv_2.d.cts +715 -0
  91. package/dist/vite.cjs +1344 -0
  92. package/dist/vite.cjs.map +1 -0
  93. package/dist/vite.d.cts +434 -0
  94. package/dist/vite.d.ts +434 -0
  95. package/dist/vite.mjs +1314 -0
  96. package/dist/vite.mjs.map +1 -0
  97. package/package.json +268 -0
  98. package/schema/extension-v2.json +1500 -0
@@ -0,0 +1,512 @@
1
+ import { JSONWebKeySet } from 'jose';
2
+ import { a as TapFilters, N as TapProjectResult, o as TapApp, Q as TapRelease, D as TapKeyword, F as TapListing, U as TapReviewsResult, v as TapCrashesResult, R as TapRevenueResult, W as TapSeriesMetric, Y as TapSeriesResult, d as TapQuery, e as TapResult, b as TapSize, T as Tap, a6 as TapToastTone, t as TapConfirmOptions, q as TapChatCard, c as TapHostEvents } from '../types-DDAGLpov.js';
3
+ import { h as TappifyHealth, b as MetricsInput, c as MetricsResponse, M as MentionItems, C as ContextBlock, W as WorkOperation, i as TappifyInstall, g as TappifyHandlerOptions, a as TappifyClaims, T as TappifyFetchHandler } from '../handler-CZIlZ5vu.js';
4
+ export { M as MatcherResult, a as MatcherTarget, r as registerTapMatchers, t as tapMatchers } from '../matchers-Cz93AK1n.js';
5
+ import { S as ScopeKey, x as MetricDeclaration, a3 as ToolDeclaration } from '../constants-DJ_KgZpN.js';
6
+ import { QueryClient } from '@tanstack/react-query';
7
+ import { RenderOptions, RenderResult } from '@testing-library/react';
8
+ import { ReactElement } from 'react';
9
+
10
+ /**
11
+ * Returns the public key set that verifies the tokens `signTestToken` signs.
12
+ *
13
+ * @remarks
14
+ * The pair is generated once per process and cached, so every call in a suite
15
+ * returns the same set. Pass it as `jwks` to `createTappifyHandler` when a test
16
+ * drives the handler itself; `createTestClient` does that for you.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { createTappifyHandler } from '@tappify/extension-sdk/server';
21
+ * import { testJwks } from '@tappify/extension-sdk/testing';
22
+ *
23
+ * async function buildHandler() {
24
+ * return createTappifyHandler({ extensionId: 'starter', jwks: await testJwks() });
25
+ * }
26
+ * ```
27
+ */
28
+ declare function testJwks(): Promise<JSONWebKeySet>;
29
+ /**
30
+ * Signs an install token a handler built against `testJwks` accepts.
31
+ *
32
+ * @remarks
33
+ * Every claim has a default — install `ins_test`, extension `starter`, project
34
+ * `prj_north`, organization `org_test`, no scopes — and the audience is
35
+ * `ext:<extensionId>`, taken from `options.extensionId`, then from the claims, then
36
+ * `starter`. It expires in five minutes unless `expiresIn` says otherwise, which is
37
+ * how a test covers an expired token. `createTestClient` and `createTapMock` sign
38
+ * their own tokens, so reach for this only when a test verifies a token itself.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * import { verifyTappifyToken } from '@tappify/extension-sdk/server';
43
+ * import { signTestToken, testJwks } from '@tappify/extension-sdk/testing';
44
+ *
45
+ * async function claimsOfTestToken() {
46
+ * const token = await signTestToken({ scopes: ['analytics:read'] });
47
+ * return verifyTappifyToken(token, {
48
+ * extensionId: 'starter',
49
+ * jwks: await testJwks(),
50
+ * });
51
+ * }
52
+ * ```
53
+ */
54
+ declare function signTestToken(claims?: Partial<TappifyClaims>, options?: {
55
+ extensionId?: string;
56
+ expiresIn?: string;
57
+ }): Promise<string>;
58
+ /** What every call a `TestClient` makes carries, unless a call overrides it. */
59
+ interface TestClientDefaults {
60
+ /** Merged over the default install, whose id is `ins_test`. */
61
+ install?: Partial<TappifyInstall>;
62
+ /** The scopes the token claims. Defaults to none. */
63
+ scopes?: string[];
64
+ /** What the handler reads as `credentials`. Defaults to empty. */
65
+ credentials?: Record<string, unknown>;
66
+ /** What the handler reads as `documents`, including `settings`. */
67
+ documents?: Record<string, unknown>;
68
+ /** The filter bar behind the call. Defaults to a seven-day September range. */
69
+ filters?: TapFilters;
70
+ /** One fixed idempotency key. Left out, each call gets its own. */
71
+ eventId?: string;
72
+ /** The origin the request url is built on. Defaults to `https://vendor.test`. */
73
+ origin?: string;
74
+ }
75
+ /** One method per route Tappify calls, plus `raw` for the response itself. */
76
+ interface TestClient {
77
+ /** Calls the health route, which carries no token. */
78
+ health(): Promise<TappifyHealth>;
79
+ /** Calls the connector's metrics route. */
80
+ metrics(input: MetricsInput): Promise<MetricsResponse>;
81
+ /** Calls one tool by id and returns the card it answered with. */
82
+ tool(id: string, input: unknown): Promise<unknown>;
83
+ /** Calls one mention by id, with the query as the `q` parameter. */
84
+ mention(id: string, query: string): Promise<MentionItems>;
85
+ /** Calls one action by id. */
86
+ action(id: string, input: unknown): Promise<unknown>;
87
+ /** Calls one procedure by name; the input defaults to an empty object. */
88
+ procedure(name: string, input?: unknown): Promise<unknown>;
89
+ /** Calls one context block by id. */
90
+ context(id: string): Promise<ContextBlock>;
91
+ /** Posts one Tappify event, which the handler answers with 204. */
92
+ event(name: string, payload: Record<string, unknown>): Promise<void>;
93
+ /** Calls one work operation. */
94
+ work(operation: WorkOperation, input: unknown): Promise<unknown>;
95
+ /**
96
+ * Sends any method and path and returns the `Response`, for a test that asserts
97
+ * on a status rather than a body.
98
+ */
99
+ raw(method: string, path: string, body?: unknown): Promise<Response>;
100
+ }
101
+ /**
102
+ * Drives a vendor server the way Tappify does, so a route is tested without a
103
+ * running Tappify.
104
+ *
105
+ * @remarks
106
+ * Pass the options object you hand `createTappifyHandler`, not the handler it
107
+ * returns: the client rebuilds the handler around `testJwks` and signs each call
108
+ * with a matching token. A built handler throws a `TapError` carrying
109
+ * `TAP_TEST_TARGET_INVALID`, because it would verify against the live Tappify key
110
+ * set and answer 401. Every call carries a fresh event id and the envelope Tappify
111
+ * sends, and every call but `health` a signed token, so the handler reads the same
112
+ * `install`, `context`, `credentials` and `documents` it will read in production. A route that
113
+ * answers an error rejects with a `TapServerError` carrying the code, message and
114
+ * status from the body, which is what a test asserts on; use `raw` when the status
115
+ * itself is the assertion. Every method calls the unprefixed path — `/tappify/…` —
116
+ * whatever `basePath` the options carry, so a `basePath` the handler strips off
117
+ * something else is not exercised here, and one that is itself a prefix of
118
+ * `/tappify`, such as `/tap`, makes every call 404 `TAP_ROUTE_UNKNOWN`. Reach a
119
+ * prefixed path through `raw` when that is what you need to cover.
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * import type { TappifyHandlerOptions } from '@tappify/extension-sdk/server';
124
+ * import { createTestClient } from '@tappify/extension-sdk/testing';
125
+ * import { expect, it } from 'vitest';
126
+ *
127
+ * const handlerOptions: TappifyHandlerOptions = {
128
+ * extensionId: 'starter',
129
+ * health: () => ({ ok: true, version: '0.1.0' }),
130
+ * };
131
+ * const client = createTestClient(handlerOptions, { scopes: ['analytics:read'] });
132
+ *
133
+ * it('answers the health check', async () => {
134
+ * await expect(client.health()).resolves.toEqual({ ok: true, version: '0.1.0' });
135
+ * });
136
+ * ```
137
+ */
138
+ declare function createTestClient(target: TappifyHandlerOptions, defaults?: TestClientDefaults): TestClient;
139
+
140
+ /**
141
+ * Sets the host's fifteen CSS variables on an element, so a component renders in
142
+ * a test the way it renders in the host.
143
+ *
144
+ * @remarks
145
+ * Sets the light values as inline custom properties, which inherit into everything
146
+ * below the element. The vitest and jest presets call it on
147
+ * `document.documentElement` for you, so call it yourself only in a test that
148
+ * renders outside `renderWithTap` or wants the variables on one subtree. It sets
149
+ * no dark values; drive the mode through `renderWithTap`'s `theme` option, which
150
+ * is what `useTapTheme` reads.
151
+ *
152
+ * @example
153
+ * ```ts
154
+ * import { installHostTheme } from '@tappify/extension-sdk/testing';
155
+ *
156
+ * function mountRoot(): HTMLElement {
157
+ * const root = document.createElement('div');
158
+ * installHostTheme(root);
159
+ * document.body.append(root);
160
+ * return root;
161
+ * }
162
+ * ```
163
+ */
164
+ declare function installHostTheme(root: HTMLElement): void;
165
+
166
+ /** The owner's two Northlight apps, one per platform. */
167
+ declare const fixtureApps: TapApp[];
168
+ /** Three shipped releases: two iOS versions and the Android build of the newer one. */
169
+ declare const fixtureReleases: TapRelease[];
170
+ /**
171
+ * Three tracked terms with positions, two on iOS and one on Android, scored on
172
+ * the host's 1 to 10 traffic scale.
173
+ */
174
+ declare const fixtureKeywords: TapKeyword[];
175
+ /** One store listing, for iOS in `en-US`. */
176
+ declare const fixtureListings: TapListing[];
177
+ /** The Northlight project, with `fixtureApps` and `fixtureReleases` under it. */
178
+ declare const fixtureProject: TapProjectResult;
179
+ /**
180
+ * Builds the seven-day series for one metric, over the first week of September
181
+ * 2026.
182
+ *
183
+ * @remarks
184
+ * The same seven values every call, so an assertion on a total or a peak is
185
+ * stable: downloads peak at 1,512 and `conversion_rate` comes back with unit
186
+ * `ratio` where the other three come back as `count`. The range in the query does
187
+ * not change the points, so a test that drives the filter bar asserts on the call,
188
+ * not on different numbers.
189
+ *
190
+ * @example
191
+ * ```ts
192
+ * import { fixtureSeries } from '@tappify/extension-sdk/testing';
193
+ *
194
+ * const downloads = fixtureSeries('downloads');
195
+ * const best = Math.max(...downloads.points.map(point => point.value));
196
+ * ```
197
+ */
198
+ declare function fixtureSeries(metric: TapSeriesMetric): TapSeriesResult;
199
+ /** Three reviews across three countries, averaging 3.7 stars, one already replied to. */
200
+ declare const fixtureReviews: TapReviewsResult;
201
+ /** A 99.42% crash-free user rate with two issues, both on build 118. */
202
+ declare const fixtureCrashes: TapCrashesResult;
203
+ /** 41,200 USD of recurring revenue, 18 refunds, and a seven-day series under it. */
204
+ declare const fixtureRevenue: TapRevenueResult;
205
+ /** The shape of `fixtures`: every sandbox fixture under one name. */
206
+ interface TapFixtures {
207
+ project: TapProjectResult;
208
+ apps: TapApp[];
209
+ releases: TapRelease[];
210
+ keywords: TapKeyword[];
211
+ listings: TapListing[];
212
+ reviews: TapReviewsResult;
213
+ crashes: TapCrashesResult;
214
+ revenue: TapRevenueResult;
215
+ /** `fixtureSeries`, so a series is built per metric rather than held. */
216
+ series(metric: TapSeriesMetric): TapSeriesResult;
217
+ }
218
+ /**
219
+ * The sandbox data the mock bridge and the portal preview both answer with, so a
220
+ * component behaves the same in a test and in the preview.
221
+ *
222
+ * @remarks
223
+ * These are the same objects the `fixture*` exports hold, gathered under one name.
224
+ * `answerFixtureQuery` clones before it answers, but reading `fixtures` directly
225
+ * hands you the live object, so a test that sorts or splices one has to copy it
226
+ * first.
227
+ *
228
+ * @example
229
+ * ```ts
230
+ * import { fixtures } from '@tappify/extension-sdk/testing';
231
+ *
232
+ * const newest = [...fixtures.releases].sort((left, right) =>
233
+ * right.version.localeCompare(left.version),
234
+ * )[0];
235
+ * ```
236
+ */
237
+ declare const fixtures: TapFixtures;
238
+ /**
239
+ * Answers one `tap.data.query` from the fixtures, which is what the mock bridge
240
+ * does.
241
+ *
242
+ * @remarks
243
+ * Every branch is cloned with `structuredClone`, so a component that sorts or
244
+ * splices a result cannot reach the fixture the next test reads. It checks no
245
+ * scope — `createTapMock` refuses an ungranted kind before it gets here — and it
246
+ * ignores the range and platform in the query, so the numbers are the same
247
+ * whatever the filter bar says. The seven assertions inside are the one place
248
+ * TypeScript cannot follow the distributive conditional in `TapResult<Q>` back
249
+ * through a `switch`; each branch returns exactly the result its `case` selected.
250
+ *
251
+ * @example
252
+ * ```ts
253
+ * import { answerFixtureQuery } from '@tappify/extension-sdk/testing';
254
+ *
255
+ * const reviews = answerFixtureQuery({
256
+ * kind: 'reviews',
257
+ * range: { from: '2026-09-01', to: '2026-09-08' },
258
+ * });
259
+ * const lowest = Math.min(...reviews.reviews.map(review => review.rating));
260
+ * ```
261
+ */
262
+ declare function answerFixtureQuery<Q extends TapQuery>(query: Q): TapResult<Q>;
263
+
264
+ /**
265
+ * Everything the component asked the host to do, in call order, for a test to
266
+ * assert on.
267
+ */
268
+ interface TapMockCalls {
269
+ /** One entry per `tap.nav.push`. */
270
+ navigations: string[];
271
+ /** One entry per `tap.nav.setSearch`. */
272
+ searches: Record<string, string | null>[];
273
+ /** One entry per `tap.ui.toast`; the tone defaults to `neutral`. */
274
+ toasts: {
275
+ message: string;
276
+ tone: TapToastTone;
277
+ }[];
278
+ /** One entry per `tap.ui.confirm`, whatever the mock answered. */
279
+ confirms: TapConfirmOptions[];
280
+ /** One entry per `tap.actions.run`, which the mock records rather than refuses. */
281
+ actions: {
282
+ actionId: string;
283
+ input: unknown;
284
+ }[];
285
+ /** One entry per `tap.telemetry.event`. */
286
+ telemetry: {
287
+ name: string;
288
+ props?: Record<string, string | number | boolean>;
289
+ }[];
290
+ /** One entry per `tap.nav.openChat`. */
291
+ chats: {
292
+ prompt: string;
293
+ context?: Record<string, unknown>;
294
+ }[];
295
+ /** One url per `tap.nav.openExternal`, with no confirmation asked. */
296
+ externals: string[];
297
+ /** The filename and byte size per `tap.ui.download`, not the blob itself. */
298
+ downloads: {
299
+ filename: string;
300
+ size: number;
301
+ }[];
302
+ /** One entry per `tap.ui.copy`, with no clipboard touched. */
303
+ copies: string[];
304
+ /** One card per `tap.ui.openInChat`, unformatted. */
305
+ cards: TapChatCard[];
306
+ /** How many times `tap.ui.expand` was called. */
307
+ expands: number;
308
+ /** How many times `tap.nav.openSettings` was called. */
309
+ settingsOpens: number;
310
+ }
311
+ /**
312
+ * How the mock bridge is built, and the options `renderWithTap` forwards to it.
313
+ */
314
+ interface CreateTapMockOptions {
315
+ /** Defaults to `ins_test`. */
316
+ installId?: string;
317
+ /** Defaults to `starter`, and has to match `handler`'s own extension id. */
318
+ extensionId?: string;
319
+ /** Defaults to `Starter`. */
320
+ extensionName?: string;
321
+ /** What the install granted. Defaults to every scope. */
322
+ scopes?: ScopeKey[];
323
+ /** What `tap.ui.size` reports. Defaults to `slot`. */
324
+ size?: TapSize;
325
+ /** Merged over the fixture filter bar. */
326
+ filters?: Partial<TapFilters>;
327
+ /** Merged over the fixture Northlight project. */
328
+ project?: Partial<Tap['project']>;
329
+ /** What `tap.context` holds. Defaults to empty. */
330
+ context?: Record<string, unknown>;
331
+ /** What `tap.params` holds, in the host's positional shape. Defaults to empty. */
332
+ params?: Record<string, string>;
333
+ /** What `useTapTheme` reports. Defaults to `light`. */
334
+ theme?: 'light' | 'dark';
335
+ /** What `tap.ui.confirm` resolves to. Defaults to `true`. */
336
+ confirmAnswer?: boolean;
337
+ /** The collections to expose, and the kind of each. Left out, every name works. */
338
+ storage?: Record<string, 'singleton' | 'collection'>;
339
+ /** One function per procedure, answering `tap.server.<name>` in process. */
340
+ server?: Record<string, (input: unknown) => unknown>;
341
+ /** Your own handler, so `tap.server.<name>` goes through its real routes. */
342
+ handler?: TappifyFetchHandler;
343
+ /** A token to send to `handler`. Defaults to one the mock signs itself. */
344
+ handlerToken?: string;
345
+ }
346
+ /** The mock bridge, with the recorded calls and the handles that drive it. */
347
+ interface TapMock {
348
+ /** The bridge itself, which `renderWithTap` puts above the component. */
349
+ tap: Tap;
350
+ calls: TapMockCalls;
351
+ /** The node `TapDialog` renders into, appended to the document by `renderWithTap`. */
352
+ portal: HTMLElement;
353
+ /** What a collection holds now, or just the one document under `documentId`. */
354
+ documents(collection: string, documentId?: string): unknown[];
355
+ /** Delivers a Tappify event to everything subscribed through `tap.data.subscribe`. */
356
+ emit<E extends keyof TapHostEvents>(event: E, payload: TapHostEvents[E]): void;
357
+ /** Replaces the whole filter bar and re-renders the hooks that read it. */
358
+ setFilters(filters: TapFilters): void;
359
+ /** Moves the mount between slot, panel and page. */
360
+ setSize(size: TapSize): void;
361
+ /** Switches the theme `useTapTheme` reports. */
362
+ setTheme(mode: 'light' | 'dark'): void;
363
+ /** Replaces the route `useTapParams` reports, in the host's own positional shape. */
364
+ setParams(params: Record<string, string>): void;
365
+ }
366
+ /**
367
+ * Builds the bridge a component talks to in a test, without rendering anything.
368
+ *
369
+ * @remarks
370
+ * Data queries answer from the sandbox fixtures, storage is an in-memory store, and
371
+ * everything else the host would do is recorded in `calls`. Scopes default to all of
372
+ * them: pass `scopes` to test the refused path, where a data query rejects with
373
+ * `TAP_SCOPE_MISSING` for the kind's own scope and every storage call, reads
374
+ * included, needs `storage:write`. A procedure needs either `server`, which answers
375
+ * in process, or `handler`, which is called over a real `Request` with a token the
376
+ * mock signs — so that handler has to be built with `jwks: await testJwks()` and the
377
+ * same `extensionId` — and without either, `tap.server.<name>` rejects with
378
+ * `TAP_UNKNOWN_PROCEDURE`. With `storage` given, a collection the map does not name
379
+ * throws `TAP_UNKNOWN_COLLECTION`. Unlike the host, this mock records
380
+ * `tap.actions.run` and resolves it as pending rather than refusing it, so a test
381
+ * can assert on the call today. Use `renderWithTap` when there is a component to
382
+ * render.
383
+ *
384
+ * @example
385
+ * ```ts
386
+ * import { createTapMock } from '@tappify/extension-sdk/testing';
387
+ * import { expect, it } from 'vitest';
388
+ *
389
+ * it('answers a keyword query from the fixtures', async () => {
390
+ * const mock = createTapMock({ scopes: ['store.metadata:read'] });
391
+ * const keywords = await mock.tap.data.query({ kind: 'keywords' });
392
+ * expect(keywords.keywords).toHaveLength(3);
393
+ * });
394
+ * ```
395
+ */
396
+ declare function createTapMock(options?: CreateTapMockOptions): TapMock;
397
+
398
+ /**
399
+ * What `renderWithTap` takes: its own three options, plus every
400
+ * `CreateTapMockOptions` member it forwards to the mock.
401
+ */
402
+ interface RenderWithTapOptions extends CreateTapMockOptions {
403
+ /** A mock you built yourself. Given, every mock option here is ignored. */
404
+ mock?: TapMock;
405
+ /** Your own client, for a test that reads the cache. Defaults to a fresh one. */
406
+ queryClient?: QueryClient;
407
+ /** Passed to Testing Library's `render`, for a mount point of your own. */
408
+ container?: RenderOptions['container'];
409
+ }
410
+ /** What `renderWithTap` returns: Testing Library's result plus the bridge. */
411
+ interface RenderWithTapResult extends RenderResult {
412
+ /** The same object `useTap` returns inside the component. */
413
+ tap: Tap;
414
+ /** The mock behind the bridge, which the matchers take. */
415
+ mock: TapMock;
416
+ /** The client the render used, whether it was passed in or built here. */
417
+ queryClient: QueryClient;
418
+ }
419
+ /**
420
+ * Renders a component against the mock bridge, so a widget, tab, page, row action
421
+ * or settings panel is testable outside Tappify.
422
+ *
423
+ * @remarks
424
+ * It builds a mock from the same options `createTapMock` takes unless you pass
425
+ * `mock`, wraps the component in the host provider and a `QueryClient` that does
426
+ * not retry and does not cache between tests, and appends the mock's portal to the
427
+ * rendered document so `TapDialog` has somewhere to go. It rewires
428
+ * `tap.invalidate` onto that client, so a component calling it drops the right
429
+ * cache entries. Scopes default to all of them, so pass `scopes` to cover the
430
+ * refused path; a `handler` has to be built with `jwks: await testJwks()` and the
431
+ * same `extensionId` the mock reports. Load
432
+ * `@tappify/extension-sdk/testing/vitest` or `.../testing/jest` as a setup file
433
+ * first: that installs the host CSS variables, registers the matchers and clears
434
+ * the portal between tests.
435
+ *
436
+ * @example
437
+ * ```tsx
438
+ * import { TapStat, useTapQuery } from '@tappify/extension-sdk';
439
+ * import { renderWithTap } from '@tappify/extension-sdk/testing';
440
+ * import { screen, waitFor } from '@testing-library/react';
441
+ * import { expect, it } from 'vitest';
442
+ *
443
+ * function Crashes() {
444
+ * const crashes = useTapQuery({
445
+ * kind: 'crashes',
446
+ * range: { from: '2026-09-01', to: '2026-09-08' },
447
+ * });
448
+ * return <TapStat label="Issues" value={crashes.data?.issues.length ?? 0} />;
449
+ * }
450
+ *
451
+ * it('renders the issues the fixtures answer with', async () => {
452
+ * renderWithTap(<Crashes />, { scopes: ['ui:render', 'crashes:read'] });
453
+ * await waitFor(() => expect(screen.getByText('2')).toBeTruthy());
454
+ * });
455
+ * ```
456
+ */
457
+ declare function renderWithTap(ui: ReactElement, options?: RenderWithTapOptions): RenderWithTapResult;
458
+
459
+ /**
460
+ * Checks a metrics response against the metrics the manifest declares and returns
461
+ * what is wrong with it.
462
+ *
463
+ * @remarks
464
+ * Returns an empty array when the response is sound, so a test asserts on an empty
465
+ * list. It reports a body that is not `{ series: [...] }`, an entry with no
466
+ * `metric` key, a metric the declarations do not name, a unit that disagrees with
467
+ * the declared one, a missing `points` array, and any point that is not a
468
+ * `[string, number]` pair. It does not check that every declared metric is
469
+ * answered, so assert on the length yourself when that matters. The strings are the
470
+ * ones `tappify extension doctor` reports.
471
+ *
472
+ * @example
473
+ * ```ts
474
+ * import { validateMetricsResponse } from '@tappify/extension-sdk/testing';
475
+ * import { expect, it } from 'vitest';
476
+ *
477
+ * const metrics = [
478
+ * { key: 'orders', label: 'Orders', unit: 'count' as const, kind: 'counter' as const },
479
+ * ];
480
+ *
481
+ * it('answers orders as a counted series', () => {
482
+ * const response = { series: [{ metric: 'orders', unit: 'count', points: [['2026-09-01', 12]] }] };
483
+ * expect(validateMetricsResponse(response, metrics)).toEqual([]);
484
+ * });
485
+ * ```
486
+ */
487
+ declare function validateMetricsResponse(response: unknown, metrics: MetricDeclaration[]): string[];
488
+ /**
489
+ * Checks a tool's answer against the card shape its declared `returns` kind has to
490
+ * carry, and returns what is missing.
491
+ *
492
+ * @remarks
493
+ * Returns an empty array when the answer is sound. A `value` card needs `label` and
494
+ * `value`, a `series` card `label` and `points`, a `table` card `columns` and
495
+ * `rows`, a `list` card `items`, and a `comparison` card `label`, `left` and
496
+ * `right`; anything that is not an object is reported on its own. It checks the
497
+ * keys are present, not what they hold, so an empty `rows` array passes.
498
+ *
499
+ * @example
500
+ * ```ts
501
+ * import { validateToolResponse } from '@tappify/extension-sdk/testing';
502
+ * import { expect, it } from 'vitest';
503
+ *
504
+ * it('answers keyword_gaps with a table card', () => {
505
+ * const card = { columns: [{ key: 'term', label: 'Term' }], rows: [] };
506
+ * expect(validateToolResponse(card, 'table')).toEqual([]);
507
+ * });
508
+ * ```
509
+ */
510
+ declare function validateToolResponse(value: unknown, returns: ToolDeclaration['returns']): string[];
511
+
512
+ export { type CreateTapMockOptions, type RenderWithTapOptions, type RenderWithTapResult, type TapFixtures, type TapMock, type TapMockCalls, type TestClient, type TestClientDefaults, answerFixtureQuery, createTapMock, createTestClient, fixtureApps, fixtureCrashes, fixtureKeywords, fixtureListings, fixtureProject, fixtureReleases, fixtureRevenue, fixtureReviews, fixtureSeries, fixtures, installHostTheme, renderWithTap, signTestToken, testJwks, validateMetricsResponse, validateToolResponse };