@tappify/extension-sdk 2.1.0-beta.2 → 2.1.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.
- package/README.md +18 -54
- package/dist/{chunk-6HETNZJY.cjs → chunk-2DBSVKAU.cjs} +1 -1
- package/dist/{chunk-6HETNZJY.cjs.map → chunk-2DBSVKAU.cjs.map} +1 -1
- package/dist/{chunk-O357OJXL.cjs → chunk-3YWVYA42.cjs} +18 -14
- package/dist/chunk-3YWVYA42.cjs.map +1 -0
- package/dist/{chunk-GAUZYBXE.cjs → chunk-5AAPD5WV.cjs} +11 -11
- package/dist/chunk-5AAPD5WV.cjs.map +1 -0
- package/dist/{chunk-SGEYDV3K.mjs → chunk-BDCX6LRQ.mjs} +3 -3
- package/dist/chunk-BDCX6LRQ.mjs.map +1 -0
- package/dist/{chunk-KZ5NGFUP.mjs → chunk-C3YNVHBP.mjs} +7 -20
- package/dist/chunk-C3YNVHBP.mjs.map +1 -0
- package/dist/{chunk-QXD7WVKK.mjs → chunk-CSTYBKZC.mjs} +1 -1
- package/dist/chunk-CSTYBKZC.mjs.map +1 -0
- package/dist/{chunk-SWG7CNTZ.mjs → chunk-CTSZ5D7S.mjs} +5 -9
- package/dist/chunk-CTSZ5D7S.mjs.map +1 -0
- package/dist/chunk-EW54H7CR.cjs +722 -0
- package/dist/chunk-EW54H7CR.cjs.map +1 -0
- package/dist/chunk-EX3NFXDK.mjs +722 -0
- package/dist/chunk-EX3NFXDK.mjs.map +1 -0
- package/dist/{chunk-A74HCO6T.cjs → chunk-G7XZSN2Q.cjs} +18 -31
- package/dist/chunk-G7XZSN2Q.cjs.map +1 -0
- package/dist/{chunk-RBIQT75L.mjs → chunk-GG3IX52W.mjs} +2 -2
- package/dist/{chunk-SLGOHIBL.mjs → chunk-IGK2AP6J.mjs} +17 -13
- package/dist/chunk-IGK2AP6J.mjs.map +1 -0
- package/dist/{chunk-VV7ELNRW.cjs → chunk-JMUK3K47.cjs} +3 -3
- package/dist/{chunk-VV7ELNRW.cjs.map → chunk-JMUK3K47.cjs.map} +1 -1
- package/dist/{chunk-SGCYEEXD.mjs → chunk-MNDVYPEP.mjs} +2 -9
- package/dist/chunk-MNDVYPEP.mjs.map +1 -0
- package/dist/{chunk-BPCGXXMD.cjs → chunk-NFUPPCX4.cjs} +18 -22
- package/dist/chunk-NFUPPCX4.cjs.map +1 -0
- package/dist/{chunk-PRCZMOLD.cjs → chunk-OPPYEA7F.cjs} +3 -3
- package/dist/{chunk-PRCZMOLD.cjs.map → chunk-OPPYEA7F.cjs.map} +1 -1
- package/dist/{chunk-7YKH4LZN.cjs → chunk-PYJMJF44.cjs} +2 -9
- package/dist/chunk-PYJMJF44.cjs.map +1 -0
- package/dist/{chunk-QA5AL3VZ.mjs → chunk-QHM7XNRK.mjs} +2 -2
- package/dist/{chunk-QA5AL3VZ.mjs.map → chunk-QHM7XNRK.mjs.map} +1 -1
- package/dist/codegen.cjs.map +1 -1
- package/dist/codegen.d.cts +7 -43
- package/dist/codegen.d.ts +7 -43
- package/dist/codegen.mjs.map +1 -1
- package/dist/{constants-Bh759FUI.d.cts → constants-DJ_KgZpN.d.cts} +8 -44
- package/dist/{constants-Bh759FUI.d.ts → constants-DJ_KgZpN.d.ts} +8 -44
- package/dist/{context-mr8R5CNt.d.ts → context-Dt6xh_5t.d.cts} +4 -5
- package/dist/{context-DI0EJCRD.d.cts → context-Ml7k_V14.d.ts} +4 -5
- package/dist/{errors-Cqh0wrqt.d.ts → errors-COsBh7kd.d.cts} +2 -3
- package/dist/{errors-Cqh0wrqt.d.cts → errors-COsBh7kd.d.ts} +2 -3
- package/dist/handler-CZIlZ5vu.d.ts +227 -0
- package/dist/handler-kU9Jf36K.d.cts +227 -0
- package/dist/host.cjs +4 -4
- package/dist/host.d.cts +4 -4
- package/dist/host.d.ts +4 -4
- package/dist/host.mjs +3 -3
- package/dist/index.cjs +34 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +627 -69
- package/dist/index.d.ts +627 -69
- package/dist/index.mjs +33 -4
- package/dist/manifest.cjs +24 -136
- package/dist/manifest.cjs.map +1 -1
- package/dist/manifest.d.cts +12 -97
- package/dist/manifest.d.ts +12 -97
- package/dist/manifest.mjs +10 -122
- package/dist/manifest.mjs.map +1 -1
- package/dist/server.cjs +66 -14
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +6 -6
- package/dist/server.d.ts +6 -6
- package/dist/server.mjs +61 -9
- package/dist/server.mjs.map +1 -1
- package/dist/styles.css +367 -0
- package/dist/testing/index.cjs +13 -72
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +10 -29
- package/dist/testing/index.d.ts +10 -29
- package/dist/testing/index.mjs +13 -72
- package/dist/testing/index.mjs.map +1 -1
- package/dist/testing/jest.cjs +5 -5
- package/dist/testing/jest.mjs +2 -2
- package/dist/testing/vitest.cjs +5 -5
- package/dist/testing/vitest.cjs.map +1 -1
- package/dist/testing/vitest.d.cts +0 -10
- package/dist/testing/vitest.d.ts +0 -10
- package/dist/testing/vitest.mjs +2 -2
- package/dist/testing/vitest.mjs.map +1 -1
- package/dist/{types-uGHrRaOj.d.ts → types-DDAGLpov.d.ts} +3 -4
- package/dist/{types-BdDaLnqs.d.cts → types-DrpQSv_2.d.cts} +3 -4
- package/dist/vite-preview.cjs +125 -294
- package/dist/vite-preview.cjs.map +1 -1
- package/dist/vite-preview.d.cts +24 -6
- package/dist/vite-preview.d.ts +24 -6
- package/dist/vite-preview.mjs +118 -287
- package/dist/vite-preview.mjs.map +1 -1
- package/dist/vite.cjs +24 -137
- package/dist/vite.cjs.map +1 -1
- package/dist/vite.d.cts +7 -43
- package/dist/vite.d.ts +7 -43
- package/dist/vite.mjs +18 -131
- package/dist/vite.mjs.map +1 -1
- package/package.json +2 -12
- package/schema/extension-v2.json +121 -440
- package/dist/chunk-2CY5HVY7.mjs +0 -59
- package/dist/chunk-2CY5HVY7.mjs.map +0 -1
- package/dist/chunk-7YKH4LZN.cjs.map +0 -1
- package/dist/chunk-A74HCO6T.cjs.map +0 -1
- package/dist/chunk-BPCGXXMD.cjs.map +0 -1
- package/dist/chunk-EB5XWQAU.cjs +0 -132
- package/dist/chunk-EB5XWQAU.cjs.map +0 -1
- package/dist/chunk-GAUZYBXE.cjs.map +0 -1
- package/dist/chunk-KZ5NGFUP.mjs.map +0 -1
- package/dist/chunk-O357OJXL.cjs.map +0 -1
- package/dist/chunk-ONJMPO3P.cjs +0 -59
- package/dist/chunk-ONJMPO3P.cjs.map +0 -1
- package/dist/chunk-QJMZRV7A.mjs +0 -132
- package/dist/chunk-QJMZRV7A.mjs.map +0 -1
- package/dist/chunk-QXD7WVKK.mjs.map +0 -1
- package/dist/chunk-SGCYEEXD.mjs.map +0 -1
- package/dist/chunk-SGEYDV3K.mjs.map +0 -1
- package/dist/chunk-SLGOHIBL.mjs.map +0 -1
- package/dist/chunk-SWG7CNTZ.mjs.map +0 -1
- package/dist/client-BO3kavn0.d.ts +0 -25
- package/dist/client-DE-vMgdO.d.cts +0 -25
- package/dist/ui/styles.css +0 -740
- package/dist/ui.cjs +0 -1018
- package/dist/ui.cjs.map +0 -1
- package/dist/ui.d.cts +0 -231
- package/dist/ui.d.ts +0 -231
- package/dist/ui.mjs +0 -1018
- package/dist/ui.mjs.map +0 -1
- package/dist/vite-preview-bootstrap.cjs +0 -66
- package/dist/vite-preview-bootstrap.cjs.map +0 -1
- package/dist/vite-preview-bootstrap.d.cts +0 -15
- package/dist/vite-preview-bootstrap.d.ts +0 -15
- package/dist/vite-preview-bootstrap.mjs +0 -66
- package/dist/vite-preview-bootstrap.mjs.map +0 -1
- package/dist/work-events-BlOVpK0k.d.ts +0 -547
- package/dist/work-events-D97EoqvS.d.cts +0 -547
- /package/dist/{chunk-RBIQT75L.mjs.map → chunk-GG3IX52W.mjs.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/testing/index.cjs","../../src/testing/render.tsx","../../src/testing/validators.ts","../../src/testing/work-events.ts"],"names":[],"mappings":"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","file":"/home/runner/work/extension-sdk/extension-sdk/dist/testing/index.cjs","sourcesContent":[null,"import { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport {\n render,\n type RenderOptions,\n type RenderResult,\n} from '@testing-library/react';\nimport type { ReactElement, ReactNode } from 'react';\nimport { installPrefix, procedurePrefix } from '../client/query';\nimport type { Tap } from '../client/types';\nimport { TapHostProvider } from '../host/provider';\nimport { createTapMock, type CreateTapMockOptions, type TapMock } from './mock';\n\n/**\n * What `renderWithTap` takes: its own three options, plus every\n * `CreateTapMockOptions` member it forwards to the mock.\n */\nexport interface RenderWithTapOptions extends CreateTapMockOptions {\n /** A mock you built yourself. Given, every mock option here is ignored. */\n mock?: TapMock;\n /** Your own client, for a test that reads the cache. Defaults to a fresh one. */\n queryClient?: QueryClient;\n /** Passed to Testing Library's `render`, for a mount point of your own. */\n container?: RenderOptions['container'];\n}\n\n/** What `renderWithTap` returns: Testing Library's result plus the bridge. */\nexport interface RenderWithTapResult extends RenderResult {\n /** The same object `useTap` returns inside the component. */\n tap: Tap;\n /** The mock behind the bridge, which the matchers take. */\n mock: TapMock;\n /** The client the render used, whether it was passed in or built here. */\n queryClient: QueryClient;\n}\n\n/**\n * Renders a component against the mock bridge, so a widget, tab, page, row action\n * or settings panel is testable outside Tappify.\n *\n * @remarks\n * It builds a mock from the same options `createTapMock` takes unless you pass\n * `mock`, wraps the component in the host provider and a `QueryClient` that does\n * not retry and does not cache between tests, and appends the mock's portal to the\n * rendered document so SDK dialogs have somewhere to go. It rewires\n * `tap.invalidate` onto that client, so a component calling it drops the right\n * cache entries. Scopes default to all of them, so pass `scopes` to cover the\n * refused path; a `handler` has to be built with `jwks: await testJwks()` and the\n * same `extensionId` the mock reports. Load\n * `@tappify/extension-sdk/testing/vitest` or `.../testing/jest` as a setup file\n * first: that installs the host CSS variables, registers the matchers and clears\n * the portal between tests.\n *\n * @example\n * ```tsx\n * import { useTapQuery } from '@tappify/extension-sdk';\n * import { Metric } from '@tappify/extension-sdk/ui';\n * import { renderWithTap } from '@tappify/extension-sdk/testing';\n * import { screen, waitFor } from '@testing-library/react';\n * import { expect, it } from 'vitest';\n *\n * function Crashes() {\n * const crashes = useTapQuery({\n * kind: 'crashes',\n * range: { from: '2026-09-01', to: '2026-09-08' },\n * });\n * return <Metric label=\"Issues\" value={crashes.data?.issues.length ?? 0} />;\n * }\n *\n * it('renders the issues the fixtures answer with', async () => {\n * renderWithTap(<Crashes />, { scopes: ['ui:render', 'crashes:read'] });\n * await waitFor(() => expect(screen.getByText('2')).toBeTruthy());\n * });\n * ```\n */\nexport function renderWithTap(\n ui: ReactElement,\n options: RenderWithTapOptions = {},\n): RenderWithTapResult {\n const mock = options.mock ?? createTapMock(options);\n const queryClient =\n options.queryClient ??\n new QueryClient({\n defaultOptions: { queries: { retry: false, gcTime: 0 } },\n });\n\n const installId = mock.tap.extension.installId;\n mock.tap.invalidate = name => {\n void queryClient.invalidateQueries({\n queryKey:\n name === undefined\n ? installPrefix(installId)\n : procedurePrefix(installId, name),\n });\n };\n\n function Wrapper({ children }: { children: ReactNode }): ReactElement {\n return (\n <QueryClientProvider client={queryClient}>\n <TapHostProvider tap={mock.tap}>{children}</TapHostProvider>\n </QueryClientProvider>\n );\n }\n\n const result = render(ui, { wrapper: Wrapper, container: options.container });\n result.baseElement.append(mock.portal);\n\n return { ...result, tap: mock.tap, mock, queryClient };\n}\n","import { CARD_KEYS } from '../manifest/constants';\nimport type { MetricDeclaration, ToolDeclaration } from '../manifest/types';\n\nfunction record(value: unknown): Record<string, unknown> | null {\n return typeof value === 'object' && value !== null\n ? (value as Record<string, unknown>)\n : null;\n}\n\nfunction list(value: unknown): unknown[] | null {\n return Array.isArray(value) ? value : null;\n}\n\nfunction isPoint(value: unknown): boolean {\n const pair = list(value);\n return (\n pair !== null &&\n pair.length === 2 &&\n typeof pair[0] === 'string' &&\n typeof pair[1] === 'number'\n );\n}\n\n/**\n * Checks a metrics response against the metrics the manifest declares and returns\n * what is wrong with it.\n *\n * @remarks\n * Returns an empty array when the response is sound, so a test asserts on an empty\n * list. It reports a body that is not `{ series: [...] }`, an entry with no\n * `metric` key, a metric the declarations do not name, a unit that disagrees with\n * the declared one, a missing `points` array, and any point that is not a\n * `[string, number]` pair. It does not check that every declared metric is\n * answered, so assert on the length yourself when that matters. The strings are the\n * ones `tappify extension doctor` reports.\n *\n * @example\n * ```ts\n * import { validateMetricsResponse } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * const metrics = [\n * { key: 'orders', label: 'Orders', unit: 'count' as const, kind: 'counter' as const },\n * ];\n *\n * it('answers orders as a counted series', () => {\n * const response = { series: [{ metric: 'orders', unit: 'count', points: [['2026-09-01', 12]] }] };\n * expect(validateMetricsResponse(response, metrics)).toEqual([]);\n * });\n * ```\n */\nexport function validateMetricsResponse(\n response: unknown,\n metrics: MetricDeclaration[],\n): string[] {\n const series = list(record(response)?.series);\n\n if (series === null) {\n return [\n 'A metrics response is { \"series\": [...] }. Return one entry per declared metric, even when it has no points.',\n ];\n }\n\n const issues: string[] = [];\n const declared = new Map(metrics.map(metric => [metric.key, metric]));\n\n for (const [index, entry] of series.entries()) {\n const item = record(entry);\n const key = typeof item?.metric === 'string' ? item.metric : null;\n\n if (item === null || key === null) {\n issues.push(`series[${String(index)}] has no \"metric\" key.`);\n continue;\n }\n\n const metric = declared.get(key);\n if (metric === undefined) {\n issues.push(\n `series[${String(index)}] reports \"${key}\", which is not declared under contributes.connector.metrics.`,\n );\n continue;\n }\n\n if (item.unit !== metric.unit) {\n issues.push(\n `series[${String(index)}] reports unit \"${String(item.unit)}\" but \"${key}\" is declared as \"${metric.unit}\".`,\n );\n }\n\n const points = list(item.points);\n if (points === null) {\n issues.push(`series[${String(index)}] has no \"points\" array.`);\n continue;\n }\n\n for (const [pointIndex, point] of points.entries()) {\n if (!isPoint(point)) {\n issues.push(\n `series[${String(index)}].points[${String(pointIndex)}] must be a two-element [timestamp, value] pair: a string and a number.`,\n );\n }\n }\n }\n\n return issues;\n}\n\n/**\n * Checks a tool's answer against the card shape its declared `returns` kind has to\n * carry, and returns what is missing.\n *\n * @remarks\n * Returns an empty array when the answer is sound. A `value` card needs `label` and\n * `value`, a `series` card `label` and `points`, a `table` card `columns` and\n * `rows`, a `list` card `items`, and a `comparison` card `label`, `left` and\n * `right`; anything that is not an object is reported on its own. It checks the\n * keys are present, not what they hold, so an empty `rows` array passes.\n *\n * @example\n * ```ts\n * import { validateToolResponse } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * it('answers keyword_gaps with a table card', () => {\n * const card = { columns: [{ key: 'term', label: 'Term' }], rows: [] };\n * expect(validateToolResponse(card, 'table')).toEqual([]);\n * });\n * ```\n */\nexport function validateToolResponse(\n value: unknown,\n returns: ToolDeclaration['returns'],\n): string[] {\n const body = record(value);\n if (body === null) {\n return [\n `A \"${returns}\" tool returns an object the host draws as a card, not ${typeof value}.`,\n ];\n }\n\n return CARD_KEYS[returns]\n .filter(key => !(key in body))\n .map(\n key =>\n `A \"${returns}\" tool response needs a \"${key}\" key; the host draws the card from it.`,\n );\n}\n","import type {\n WorkHydratedCommentCreatedEvent,\n WorkHydratedItemDeletedEvent,\n WorkHydratedStatusChangedEvent,\n} from '../server/work';\nimport {\n sendWorkEvent,\n type SendWorkEventOptions,\n type WorkEvent,\n} from '../server/work-events';\n\n/** A fresh complete comment signal with deterministic identity and shallow overrides. */\nexport function workCommentCreatedFixture(\n overrides: Partial<WorkHydratedCommentCreatedEvent> = {},\n): WorkHydratedCommentCreatedEvent {\n return {\n type: 'comment.created',\n externalItemId: 'item_test',\n providerEventId: 'event_comment_test',\n occurredAt: '2026-10-15T12:00:00.000Z',\n dedupeKey: 'item_test:event_comment_test',\n comment: {\n externalCommentId: 'comment_test',\n body: 'A provider comment.',\n author: { id: 'author_test', displayName: 'Sam' },\n createdAt: '2026-10-15T12:00:00.000Z',\n },\n ...overrides,\n };\n}\n\n/** A fresh complete status signal with deterministic identity and shallow overrides. */\nexport function workStatusChangedFixture(\n overrides: Partial<WorkHydratedStatusChangedEvent> = {},\n): WorkHydratedStatusChangedEvent {\n return {\n type: 'status.changed',\n externalItemId: 'item_test',\n providerEventId: 'event_status_test',\n occurredAt: '2026-10-15T12:00:00.000Z',\n dedupeKey: 'item_test:event_status_test',\n status: { id: 'resolved_test', label: 'Resolved', stage: 'done' },\n ...overrides,\n };\n}\n\n/** A fresh deletion signal with deterministic identity and shallow overrides. */\nexport function workItemDeletedFixture(\n overrides: Partial<WorkHydratedItemDeletedEvent> = {},\n): WorkHydratedItemDeletedEvent {\n return {\n type: 'item.deleted',\n externalItemId: 'item_test',\n providerEventId: 'event_deleted_test',\n occurredAt: '2026-10-15T12:00:00.000Z',\n dedupeKey: 'item_test:event_deleted_test',\n ...overrides,\n };\n}\n\n/**\n * Captures a signed work-event Request through the real sender without network I/O.\n *\n * @remarks\n * Uses the same validation, serialization, signing and callback URL as\n * sendWorkEvent. Supply the callback and secret your test install was configured\n * with; the accepting transport is supplied by this helper.\n */\nexport async function signedWorkEventRequest(\n event: WorkEvent,\n options: Omit<SendWorkEventOptions, 'fetch'>,\n): Promise<Request> {\n let request: Request | undefined;\n await sendWorkEvent(event, {\n ...options,\n fetch: (input, init) => {\n request = new Request(input, init);\n return Promise.resolve(new Response(null, { status: 202 }));\n },\n });\n if (!request)\n throw new Error('The work event sender did not produce a request.');\n return request;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/testing/index.cjs","../../src/testing/render.tsx","../../src/testing/validators.ts"],"names":[],"mappings":"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","file":"/home/runner/work/extension-sdk/extension-sdk/dist/testing/index.cjs","sourcesContent":[null,"import { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport {\n render,\n type RenderOptions,\n type RenderResult,\n} from '@testing-library/react';\nimport type { ReactElement, ReactNode } from 'react';\nimport { installPrefix, procedurePrefix } from '../client/query';\nimport type { Tap } from '../client/types';\nimport { TapHostProvider } from '../host/provider';\nimport { createTapMock, type CreateTapMockOptions, type TapMock } from './mock';\n\n/**\n * What `renderWithTap` takes: its own three options, plus every\n * `CreateTapMockOptions` member it forwards to the mock.\n */\nexport interface RenderWithTapOptions extends CreateTapMockOptions {\n /** A mock you built yourself. Given, every mock option here is ignored. */\n mock?: TapMock;\n /** Your own client, for a test that reads the cache. Defaults to a fresh one. */\n queryClient?: QueryClient;\n /** Passed to Testing Library's `render`, for a mount point of your own. */\n container?: RenderOptions['container'];\n}\n\n/** What `renderWithTap` returns: Testing Library's result plus the bridge. */\nexport interface RenderWithTapResult extends RenderResult {\n /** The same object `useTap` returns inside the component. */\n tap: Tap;\n /** The mock behind the bridge, which the matchers take. */\n mock: TapMock;\n /** The client the render used, whether it was passed in or built here. */\n queryClient: QueryClient;\n}\n\n/**\n * Renders a component against the mock bridge, so a widget, tab, page, row action\n * or settings panel is testable outside Tappify.\n *\n * @remarks\n * It builds a mock from the same options `createTapMock` takes unless you pass\n * `mock`, wraps the component in the host provider and a `QueryClient` that does\n * not retry and does not cache between tests, and appends the mock's portal to the\n * rendered document so `TapDialog` has somewhere to go. It rewires\n * `tap.invalidate` onto that client, so a component calling it drops the right\n * cache entries. Scopes default to all of them, so pass `scopes` to cover the\n * refused path; a `handler` has to be built with `jwks: await testJwks()` and the\n * same `extensionId` the mock reports. Load\n * `@tappify/extension-sdk/testing/vitest` or `.../testing/jest` as a setup file\n * first: that installs the host CSS variables, registers the matchers and clears\n * the portal between tests.\n *\n * @example\n * ```tsx\n * import { TapStat, useTapQuery } from '@tappify/extension-sdk';\n * import { renderWithTap } from '@tappify/extension-sdk/testing';\n * import { screen, waitFor } from '@testing-library/react';\n * import { expect, it } from 'vitest';\n *\n * function Crashes() {\n * const crashes = useTapQuery({\n * kind: 'crashes',\n * range: { from: '2026-09-01', to: '2026-09-08' },\n * });\n * return <TapStat label=\"Issues\" value={crashes.data?.issues.length ?? 0} />;\n * }\n *\n * it('renders the issues the fixtures answer with', async () => {\n * renderWithTap(<Crashes />, { scopes: ['ui:render', 'crashes:read'] });\n * await waitFor(() => expect(screen.getByText('2')).toBeTruthy());\n * });\n * ```\n */\nexport function renderWithTap(\n ui: ReactElement,\n options: RenderWithTapOptions = {},\n): RenderWithTapResult {\n const mock = options.mock ?? createTapMock(options);\n const queryClient =\n options.queryClient ??\n new QueryClient({\n defaultOptions: { queries: { retry: false, gcTime: 0 } },\n });\n\n const installId = mock.tap.extension.installId;\n mock.tap.invalidate = name => {\n void queryClient.invalidateQueries({\n queryKey:\n name === undefined\n ? installPrefix(installId)\n : procedurePrefix(installId, name),\n });\n };\n\n function Wrapper({ children }: { children: ReactNode }): ReactElement {\n return (\n <QueryClientProvider client={queryClient}>\n <TapHostProvider tap={mock.tap}>{children}</TapHostProvider>\n </QueryClientProvider>\n );\n }\n\n const result = render(ui, { wrapper: Wrapper, container: options.container });\n result.baseElement.append(mock.portal);\n\n return { ...result, tap: mock.tap, mock, queryClient };\n}\n","import { CARD_KEYS } from '../manifest/constants';\nimport type { MetricDeclaration, ToolDeclaration } from '../manifest/types';\n\nfunction record(value: unknown): Record<string, unknown> | null {\n return typeof value === 'object' && value !== null\n ? (value as Record<string, unknown>)\n : null;\n}\n\nfunction list(value: unknown): unknown[] | null {\n return Array.isArray(value) ? value : null;\n}\n\nfunction isPoint(value: unknown): boolean {\n const pair = list(value);\n return (\n pair !== null &&\n pair.length === 2 &&\n typeof pair[0] === 'string' &&\n typeof pair[1] === 'number'\n );\n}\n\n/**\n * Checks a metrics response against the metrics the manifest declares and returns\n * what is wrong with it.\n *\n * @remarks\n * Returns an empty array when the response is sound, so a test asserts on an empty\n * list. It reports a body that is not `{ series: [...] }`, an entry with no\n * `metric` key, a metric the declarations do not name, a unit that disagrees with\n * the declared one, a missing `points` array, and any point that is not a\n * `[string, number]` pair. It does not check that every declared metric is\n * answered, so assert on the length yourself when that matters. The strings are the\n * ones `tappify extension doctor` reports.\n *\n * @example\n * ```ts\n * import { validateMetricsResponse } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * const metrics = [\n * { key: 'orders', label: 'Orders', unit: 'count' as const, kind: 'counter' as const },\n * ];\n *\n * it('answers orders as a counted series', () => {\n * const response = { series: [{ metric: 'orders', unit: 'count', points: [['2026-09-01', 12]] }] };\n * expect(validateMetricsResponse(response, metrics)).toEqual([]);\n * });\n * ```\n */\nexport function validateMetricsResponse(\n response: unknown,\n metrics: MetricDeclaration[],\n): string[] {\n const series = list(record(response)?.series);\n\n if (series === null) {\n return [\n 'A metrics response is { \"series\": [...] }. Return one entry per declared metric, even when it has no points.',\n ];\n }\n\n const issues: string[] = [];\n const declared = new Map(metrics.map(metric => [metric.key, metric]));\n\n for (const [index, entry] of series.entries()) {\n const item = record(entry);\n const key = typeof item?.metric === 'string' ? item.metric : null;\n\n if (item === null || key === null) {\n issues.push(`series[${String(index)}] has no \"metric\" key.`);\n continue;\n }\n\n const metric = declared.get(key);\n if (metric === undefined) {\n issues.push(\n `series[${String(index)}] reports \"${key}\", which is not declared under contributes.connector.metrics.`,\n );\n continue;\n }\n\n if (item.unit !== metric.unit) {\n issues.push(\n `series[${String(index)}] reports unit \"${String(item.unit)}\" but \"${key}\" is declared as \"${metric.unit}\".`,\n );\n }\n\n const points = list(item.points);\n if (points === null) {\n issues.push(`series[${String(index)}] has no \"points\" array.`);\n continue;\n }\n\n for (const [pointIndex, point] of points.entries()) {\n if (!isPoint(point)) {\n issues.push(\n `series[${String(index)}].points[${String(pointIndex)}] must be a two-element [timestamp, value] pair: a string and a number.`,\n );\n }\n }\n }\n\n return issues;\n}\n\n/**\n * Checks a tool's answer against the card shape its declared `returns` kind has to\n * carry, and returns what is missing.\n *\n * @remarks\n * Returns an empty array when the answer is sound. A `value` card needs `label` and\n * `value`, a `series` card `label` and `points`, a `table` card `columns` and\n * `rows`, a `list` card `items`, and a `comparison` card `label`, `left` and\n * `right`; anything that is not an object is reported on its own. It checks the\n * keys are present, not what they hold, so an empty `rows` array passes.\n *\n * @example\n * ```ts\n * import { validateToolResponse } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * it('answers keyword_gaps with a table card', () => {\n * const card = { columns: [{ key: 'term', label: 'Term' }], rows: [] };\n * expect(validateToolResponse(card, 'table')).toEqual([]);\n * });\n * ```\n */\nexport function validateToolResponse(\n value: unknown,\n returns: ToolDeclaration['returns'],\n): string[] {\n const body = record(value);\n if (body === null) {\n return [\n `A \"${returns}\" tool returns an object the host draws as a card, not ${typeof value}.`,\n ];\n }\n\n return CARD_KEYS[returns]\n .filter(key => !(key in body))\n .map(\n key =>\n `A \"${returns}\" tool response needs a \"${key}\" key; the host draws the card from it.`,\n );\n}\n"]}
|
package/dist/testing/index.d.cts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
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-
|
|
3
|
-
import {
|
|
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-DrpQSv_2.cjs';
|
|
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-kU9Jf36K.cjs';
|
|
4
4
|
export { M as MatcherResult, a as MatcherTarget, r as registerTapMatchers, t as tapMatchers } from '../matchers-Cz93AK1n.cjs';
|
|
5
|
-
import { S as ScopeKey, x as MetricDeclaration,
|
|
5
|
+
import { S as ScopeKey, x as MetricDeclaration, a3 as ToolDeclaration } from '../constants-DJ_KgZpN.cjs';
|
|
6
6
|
import { QueryClient } from '@tanstack/react-query';
|
|
7
7
|
import { RenderOptions, RenderResult } from '@testing-library/react';
|
|
8
8
|
import { ReactElement } from 'react';
|
|
@@ -91,9 +91,7 @@ interface TestClient {
|
|
|
91
91
|
/** Posts one Tappify event, which the handler answers with 204. */
|
|
92
92
|
event(name: string, payload: Record<string, unknown>): Promise<void>;
|
|
93
93
|
/** Calls one work operation. */
|
|
94
|
-
work
|
|
95
|
-
/** @deprecated Call set_status or describe_container using the current work protocol. */
|
|
96
|
-
work(operation: 'status' | 'resolve', input: unknown): Promise<unknown>;
|
|
94
|
+
work(operation: WorkOperation, input: unknown): Promise<unknown>;
|
|
97
95
|
/**
|
|
98
96
|
* Sends any method and path and returns the `Response`, for a test that asserts
|
|
99
97
|
* on a status rather than a body.
|
|
@@ -140,7 +138,7 @@ interface TestClient {
|
|
|
140
138
|
declare function createTestClient(target: TappifyHandlerOptions, defaults?: TestClientDefaults): TestClient;
|
|
141
139
|
|
|
142
140
|
/**
|
|
143
|
-
* Sets the host's
|
|
141
|
+
* Sets the host's fifteen CSS variables on an element, so a component renders in
|
|
144
142
|
* a test the way it renders in the host.
|
|
145
143
|
*
|
|
146
144
|
* @remarks
|
|
@@ -350,7 +348,7 @@ interface TapMock {
|
|
|
350
348
|
/** The bridge itself, which `renderWithTap` puts above the component. */
|
|
351
349
|
tap: Tap;
|
|
352
350
|
calls: TapMockCalls;
|
|
353
|
-
/** The node
|
|
351
|
+
/** The node `TapDialog` renders into, appended to the document by `renderWithTap`. */
|
|
354
352
|
portal: HTMLElement;
|
|
355
353
|
/** What a collection holds now, or just the one document under `documentId`. */
|
|
356
354
|
documents(collection: string, documentId?: string): unknown[];
|
|
@@ -426,7 +424,7 @@ interface RenderWithTapResult extends RenderResult {
|
|
|
426
424
|
* It builds a mock from the same options `createTapMock` takes unless you pass
|
|
427
425
|
* `mock`, wraps the component in the host provider and a `QueryClient` that does
|
|
428
426
|
* not retry and does not cache between tests, and appends the mock's portal to the
|
|
429
|
-
* rendered document so
|
|
427
|
+
* rendered document so `TapDialog` has somewhere to go. It rewires
|
|
430
428
|
* `tap.invalidate` onto that client, so a component calling it drops the right
|
|
431
429
|
* cache entries. Scopes default to all of them, so pass `scopes` to cover the
|
|
432
430
|
* refused path; a `handler` has to be built with `jwks: await testJwks()` and the
|
|
@@ -437,8 +435,7 @@ interface RenderWithTapResult extends RenderResult {
|
|
|
437
435
|
*
|
|
438
436
|
* @example
|
|
439
437
|
* ```tsx
|
|
440
|
-
* import { useTapQuery } from '@tappify/extension-sdk';
|
|
441
|
-
* import { Metric } from '@tappify/extension-sdk/ui';
|
|
438
|
+
* import { TapStat, useTapQuery } from '@tappify/extension-sdk';
|
|
442
439
|
* import { renderWithTap } from '@tappify/extension-sdk/testing';
|
|
443
440
|
* import { screen, waitFor } from '@testing-library/react';
|
|
444
441
|
* import { expect, it } from 'vitest';
|
|
@@ -448,7 +445,7 @@ interface RenderWithTapResult extends RenderResult {
|
|
|
448
445
|
* kind: 'crashes',
|
|
449
446
|
* range: { from: '2026-09-01', to: '2026-09-08' },
|
|
450
447
|
* });
|
|
451
|
-
* return <
|
|
448
|
+
* return <TapStat label="Issues" value={crashes.data?.issues.length ?? 0} />;
|
|
452
449
|
* }
|
|
453
450
|
*
|
|
454
451
|
* it('renders the issues the fixtures answer with', async () => {
|
|
@@ -512,20 +509,4 @@ declare function validateMetricsResponse(response: unknown, metrics: MetricDecla
|
|
|
512
509
|
*/
|
|
513
510
|
declare function validateToolResponse(value: unknown, returns: ToolDeclaration['returns']): string[];
|
|
514
511
|
|
|
515
|
-
|
|
516
|
-
declare function workCommentCreatedFixture(overrides?: Partial<WorkHydratedCommentCreatedEvent>): WorkHydratedCommentCreatedEvent;
|
|
517
|
-
/** A fresh complete status signal with deterministic identity and shallow overrides. */
|
|
518
|
-
declare function workStatusChangedFixture(overrides?: Partial<WorkHydratedStatusChangedEvent>): WorkHydratedStatusChangedEvent;
|
|
519
|
-
/** A fresh deletion signal with deterministic identity and shallow overrides. */
|
|
520
|
-
declare function workItemDeletedFixture(overrides?: Partial<WorkHydratedItemDeletedEvent>): WorkHydratedItemDeletedEvent;
|
|
521
|
-
/**
|
|
522
|
-
* Captures a signed work-event Request through the real sender without network I/O.
|
|
523
|
-
*
|
|
524
|
-
* @remarks
|
|
525
|
-
* Uses the same validation, serialization, signing and callback URL as
|
|
526
|
-
* sendWorkEvent. Supply the callback and secret your test install was configured
|
|
527
|
-
* with; the accepting transport is supplied by this helper.
|
|
528
|
-
*/
|
|
529
|
-
declare function signedWorkEventRequest(event: WorkEvent, options: Omit<SendWorkEventOptions, 'fetch'>): Promise<Request>;
|
|
530
|
-
|
|
531
|
-
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, signedWorkEventRequest, testJwks, validateMetricsResponse, validateToolResponse, workCommentCreatedFixture, workItemDeletedFixture, workStatusChangedFixture };
|
|
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 };
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
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-
|
|
3
|
-
import {
|
|
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
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,
|
|
5
|
+
import { S as ScopeKey, x as MetricDeclaration, a3 as ToolDeclaration } from '../constants-DJ_KgZpN.js';
|
|
6
6
|
import { QueryClient } from '@tanstack/react-query';
|
|
7
7
|
import { RenderOptions, RenderResult } from '@testing-library/react';
|
|
8
8
|
import { ReactElement } from 'react';
|
|
@@ -91,9 +91,7 @@ interface TestClient {
|
|
|
91
91
|
/** Posts one Tappify event, which the handler answers with 204. */
|
|
92
92
|
event(name: string, payload: Record<string, unknown>): Promise<void>;
|
|
93
93
|
/** Calls one work operation. */
|
|
94
|
-
work
|
|
95
|
-
/** @deprecated Call set_status or describe_container using the current work protocol. */
|
|
96
|
-
work(operation: 'status' | 'resolve', input: unknown): Promise<unknown>;
|
|
94
|
+
work(operation: WorkOperation, input: unknown): Promise<unknown>;
|
|
97
95
|
/**
|
|
98
96
|
* Sends any method and path and returns the `Response`, for a test that asserts
|
|
99
97
|
* on a status rather than a body.
|
|
@@ -140,7 +138,7 @@ interface TestClient {
|
|
|
140
138
|
declare function createTestClient(target: TappifyHandlerOptions, defaults?: TestClientDefaults): TestClient;
|
|
141
139
|
|
|
142
140
|
/**
|
|
143
|
-
* Sets the host's
|
|
141
|
+
* Sets the host's fifteen CSS variables on an element, so a component renders in
|
|
144
142
|
* a test the way it renders in the host.
|
|
145
143
|
*
|
|
146
144
|
* @remarks
|
|
@@ -350,7 +348,7 @@ interface TapMock {
|
|
|
350
348
|
/** The bridge itself, which `renderWithTap` puts above the component. */
|
|
351
349
|
tap: Tap;
|
|
352
350
|
calls: TapMockCalls;
|
|
353
|
-
/** The node
|
|
351
|
+
/** The node `TapDialog` renders into, appended to the document by `renderWithTap`. */
|
|
354
352
|
portal: HTMLElement;
|
|
355
353
|
/** What a collection holds now, or just the one document under `documentId`. */
|
|
356
354
|
documents(collection: string, documentId?: string): unknown[];
|
|
@@ -426,7 +424,7 @@ interface RenderWithTapResult extends RenderResult {
|
|
|
426
424
|
* It builds a mock from the same options `createTapMock` takes unless you pass
|
|
427
425
|
* `mock`, wraps the component in the host provider and a `QueryClient` that does
|
|
428
426
|
* not retry and does not cache between tests, and appends the mock's portal to the
|
|
429
|
-
* rendered document so
|
|
427
|
+
* rendered document so `TapDialog` has somewhere to go. It rewires
|
|
430
428
|
* `tap.invalidate` onto that client, so a component calling it drops the right
|
|
431
429
|
* cache entries. Scopes default to all of them, so pass `scopes` to cover the
|
|
432
430
|
* refused path; a `handler` has to be built with `jwks: await testJwks()` and the
|
|
@@ -437,8 +435,7 @@ interface RenderWithTapResult extends RenderResult {
|
|
|
437
435
|
*
|
|
438
436
|
* @example
|
|
439
437
|
* ```tsx
|
|
440
|
-
* import { useTapQuery } from '@tappify/extension-sdk';
|
|
441
|
-
* import { Metric } from '@tappify/extension-sdk/ui';
|
|
438
|
+
* import { TapStat, useTapQuery } from '@tappify/extension-sdk';
|
|
442
439
|
* import { renderWithTap } from '@tappify/extension-sdk/testing';
|
|
443
440
|
* import { screen, waitFor } from '@testing-library/react';
|
|
444
441
|
* import { expect, it } from 'vitest';
|
|
@@ -448,7 +445,7 @@ interface RenderWithTapResult extends RenderResult {
|
|
|
448
445
|
* kind: 'crashes',
|
|
449
446
|
* range: { from: '2026-09-01', to: '2026-09-08' },
|
|
450
447
|
* });
|
|
451
|
-
* return <
|
|
448
|
+
* return <TapStat label="Issues" value={crashes.data?.issues.length ?? 0} />;
|
|
452
449
|
* }
|
|
453
450
|
*
|
|
454
451
|
* it('renders the issues the fixtures answer with', async () => {
|
|
@@ -512,20 +509,4 @@ declare function validateMetricsResponse(response: unknown, metrics: MetricDecla
|
|
|
512
509
|
*/
|
|
513
510
|
declare function validateToolResponse(value: unknown, returns: ToolDeclaration['returns']): string[];
|
|
514
511
|
|
|
515
|
-
|
|
516
|
-
declare function workCommentCreatedFixture(overrides?: Partial<WorkHydratedCommentCreatedEvent>): WorkHydratedCommentCreatedEvent;
|
|
517
|
-
/** A fresh complete status signal with deterministic identity and shallow overrides. */
|
|
518
|
-
declare function workStatusChangedFixture(overrides?: Partial<WorkHydratedStatusChangedEvent>): WorkHydratedStatusChangedEvent;
|
|
519
|
-
/** A fresh deletion signal with deterministic identity and shallow overrides. */
|
|
520
|
-
declare function workItemDeletedFixture(overrides?: Partial<WorkHydratedItemDeletedEvent>): WorkHydratedItemDeletedEvent;
|
|
521
|
-
/**
|
|
522
|
-
* Captures a signed work-event Request through the real sender without network I/O.
|
|
523
|
-
*
|
|
524
|
-
* @remarks
|
|
525
|
-
* Uses the same validation, serialization, signing and callback URL as
|
|
526
|
-
* sendWorkEvent. Supply the callback and secret your test install was configured
|
|
527
|
-
* with; the accepting transport is supplied by this helper.
|
|
528
|
-
*/
|
|
529
|
-
declare function signedWorkEventRequest(event: WorkEvent, options: Omit<SendWorkEventOptions, 'fetch'>): Promise<Request>;
|
|
530
|
-
|
|
531
|
-
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, signedWorkEventRequest, testJwks, validateMetricsResponse, validateToolResponse, workCommentCreatedFixture, workItemDeletedFixture, workStatusChangedFixture };
|
|
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 };
|
package/dist/testing/index.mjs
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
installHostTheme,
|
|
3
|
-
registerTapMatchers,
|
|
4
|
-
tapMatchers
|
|
5
|
-
} from "../chunk-SLGOHIBL.mjs";
|
|
6
1
|
import {
|
|
7
2
|
answerFixtureQuery,
|
|
8
3
|
createTapMock,
|
|
@@ -19,23 +14,25 @@ import {
|
|
|
19
14
|
fixtures,
|
|
20
15
|
signTestToken,
|
|
21
16
|
testJwks
|
|
22
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-CTSZ5D7S.mjs";
|
|
23
18
|
import {
|
|
24
19
|
installPrefix,
|
|
25
20
|
procedurePrefix
|
|
26
|
-
} from "../chunk-
|
|
21
|
+
} from "../chunk-BDCX6LRQ.mjs";
|
|
27
22
|
import {
|
|
28
23
|
TapHostProvider
|
|
29
|
-
} from "../chunk-
|
|
30
|
-
import "../chunk-
|
|
31
|
-
import
|
|
32
|
-
sendWorkEvent
|
|
33
|
-
} from "../chunk-QJMZRV7A.mjs";
|
|
34
|
-
import "../chunk-KZ5NGFUP.mjs";
|
|
24
|
+
} from "../chunk-GG3IX52W.mjs";
|
|
25
|
+
import "../chunk-QHM7XNRK.mjs";
|
|
26
|
+
import "../chunk-C3YNVHBP.mjs";
|
|
35
27
|
import {
|
|
36
28
|
CARD_KEYS
|
|
37
|
-
} from "../chunk-
|
|
38
|
-
import
|
|
29
|
+
} from "../chunk-MNDVYPEP.mjs";
|
|
30
|
+
import {
|
|
31
|
+
installHostTheme,
|
|
32
|
+
registerTapMatchers,
|
|
33
|
+
tapMatchers
|
|
34
|
+
} from "../chunk-IGK2AP6J.mjs";
|
|
35
|
+
import "../chunk-CSTYBKZC.mjs";
|
|
39
36
|
import "../chunk-J5LGTIGS.mjs";
|
|
40
37
|
|
|
41
38
|
// src/testing/render.tsx
|
|
@@ -128,58 +125,6 @@ function validateToolResponse(value, returns) {
|
|
|
128
125
|
(key) => `A "${returns}" tool response needs a "${key}" key; the host draws the card from it.`
|
|
129
126
|
);
|
|
130
127
|
}
|
|
131
|
-
|
|
132
|
-
// src/testing/work-events.ts
|
|
133
|
-
function workCommentCreatedFixture(overrides = {}) {
|
|
134
|
-
return {
|
|
135
|
-
type: "comment.created",
|
|
136
|
-
externalItemId: "item_test",
|
|
137
|
-
providerEventId: "event_comment_test",
|
|
138
|
-
occurredAt: "2026-10-15T12:00:00.000Z",
|
|
139
|
-
dedupeKey: "item_test:event_comment_test",
|
|
140
|
-
comment: {
|
|
141
|
-
externalCommentId: "comment_test",
|
|
142
|
-
body: "A provider comment.",
|
|
143
|
-
author: { id: "author_test", displayName: "Sam" },
|
|
144
|
-
createdAt: "2026-10-15T12:00:00.000Z"
|
|
145
|
-
},
|
|
146
|
-
...overrides
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
function workStatusChangedFixture(overrides = {}) {
|
|
150
|
-
return {
|
|
151
|
-
type: "status.changed",
|
|
152
|
-
externalItemId: "item_test",
|
|
153
|
-
providerEventId: "event_status_test",
|
|
154
|
-
occurredAt: "2026-10-15T12:00:00.000Z",
|
|
155
|
-
dedupeKey: "item_test:event_status_test",
|
|
156
|
-
status: { id: "resolved_test", label: "Resolved", stage: "done" },
|
|
157
|
-
...overrides
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
function workItemDeletedFixture(overrides = {}) {
|
|
161
|
-
return {
|
|
162
|
-
type: "item.deleted",
|
|
163
|
-
externalItemId: "item_test",
|
|
164
|
-
providerEventId: "event_deleted_test",
|
|
165
|
-
occurredAt: "2026-10-15T12:00:00.000Z",
|
|
166
|
-
dedupeKey: "item_test:event_deleted_test",
|
|
167
|
-
...overrides
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
async function signedWorkEventRequest(event, options) {
|
|
171
|
-
let request;
|
|
172
|
-
await sendWorkEvent(event, {
|
|
173
|
-
...options,
|
|
174
|
-
fetch: (input, init) => {
|
|
175
|
-
request = new Request(input, init);
|
|
176
|
-
return Promise.resolve(new Response(null, { status: 202 }));
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
if (!request)
|
|
180
|
-
throw new Error("The work event sender did not produce a request.");
|
|
181
|
-
return request;
|
|
182
|
-
}
|
|
183
128
|
export {
|
|
184
129
|
answerFixtureQuery,
|
|
185
130
|
createTapMock,
|
|
@@ -198,13 +143,9 @@ export {
|
|
|
198
143
|
registerTapMatchers,
|
|
199
144
|
renderWithTap,
|
|
200
145
|
signTestToken,
|
|
201
|
-
signedWorkEventRequest,
|
|
202
146
|
tapMatchers,
|
|
203
147
|
testJwks,
|
|
204
148
|
validateMetricsResponse,
|
|
205
|
-
validateToolResponse
|
|
206
|
-
workCommentCreatedFixture,
|
|
207
|
-
workItemDeletedFixture,
|
|
208
|
-
workStatusChangedFixture
|
|
149
|
+
validateToolResponse
|
|
209
150
|
};
|
|
210
151
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/testing/render.tsx","../../src/testing/validators.ts","../../src/testing/work-events.ts"],"sourcesContent":["import { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport {\n render,\n type RenderOptions,\n type RenderResult,\n} from '@testing-library/react';\nimport type { ReactElement, ReactNode } from 'react';\nimport { installPrefix, procedurePrefix } from '../client/query';\nimport type { Tap } from '../client/types';\nimport { TapHostProvider } from '../host/provider';\nimport { createTapMock, type CreateTapMockOptions, type TapMock } from './mock';\n\n/**\n * What `renderWithTap` takes: its own three options, plus every\n * `CreateTapMockOptions` member it forwards to the mock.\n */\nexport interface RenderWithTapOptions extends CreateTapMockOptions {\n /** A mock you built yourself. Given, every mock option here is ignored. */\n mock?: TapMock;\n /** Your own client, for a test that reads the cache. Defaults to a fresh one. */\n queryClient?: QueryClient;\n /** Passed to Testing Library's `render`, for a mount point of your own. */\n container?: RenderOptions['container'];\n}\n\n/** What `renderWithTap` returns: Testing Library's result plus the bridge. */\nexport interface RenderWithTapResult extends RenderResult {\n /** The same object `useTap` returns inside the component. */\n tap: Tap;\n /** The mock behind the bridge, which the matchers take. */\n mock: TapMock;\n /** The client the render used, whether it was passed in or built here. */\n queryClient: QueryClient;\n}\n\n/**\n * Renders a component against the mock bridge, so a widget, tab, page, row action\n * or settings panel is testable outside Tappify.\n *\n * @remarks\n * It builds a mock from the same options `createTapMock` takes unless you pass\n * `mock`, wraps the component in the host provider and a `QueryClient` that does\n * not retry and does not cache between tests, and appends the mock's portal to the\n * rendered document so SDK dialogs have somewhere to go. It rewires\n * `tap.invalidate` onto that client, so a component calling it drops the right\n * cache entries. Scopes default to all of them, so pass `scopes` to cover the\n * refused path; a `handler` has to be built with `jwks: await testJwks()` and the\n * same `extensionId` the mock reports. Load\n * `@tappify/extension-sdk/testing/vitest` or `.../testing/jest` as a setup file\n * first: that installs the host CSS variables, registers the matchers and clears\n * the portal between tests.\n *\n * @example\n * ```tsx\n * import { useTapQuery } from '@tappify/extension-sdk';\n * import { Metric } from '@tappify/extension-sdk/ui';\n * import { renderWithTap } from '@tappify/extension-sdk/testing';\n * import { screen, waitFor } from '@testing-library/react';\n * import { expect, it } from 'vitest';\n *\n * function Crashes() {\n * const crashes = useTapQuery({\n * kind: 'crashes',\n * range: { from: '2026-09-01', to: '2026-09-08' },\n * });\n * return <Metric label=\"Issues\" value={crashes.data?.issues.length ?? 0} />;\n * }\n *\n * it('renders the issues the fixtures answer with', async () => {\n * renderWithTap(<Crashes />, { scopes: ['ui:render', 'crashes:read'] });\n * await waitFor(() => expect(screen.getByText('2')).toBeTruthy());\n * });\n * ```\n */\nexport function renderWithTap(\n ui: ReactElement,\n options: RenderWithTapOptions = {},\n): RenderWithTapResult {\n const mock = options.mock ?? createTapMock(options);\n const queryClient =\n options.queryClient ??\n new QueryClient({\n defaultOptions: { queries: { retry: false, gcTime: 0 } },\n });\n\n const installId = mock.tap.extension.installId;\n mock.tap.invalidate = name => {\n void queryClient.invalidateQueries({\n queryKey:\n name === undefined\n ? installPrefix(installId)\n : procedurePrefix(installId, name),\n });\n };\n\n function Wrapper({ children }: { children: ReactNode }): ReactElement {\n return (\n <QueryClientProvider client={queryClient}>\n <TapHostProvider tap={mock.tap}>{children}</TapHostProvider>\n </QueryClientProvider>\n );\n }\n\n const result = render(ui, { wrapper: Wrapper, container: options.container });\n result.baseElement.append(mock.portal);\n\n return { ...result, tap: mock.tap, mock, queryClient };\n}\n","import { CARD_KEYS } from '../manifest/constants';\nimport type { MetricDeclaration, ToolDeclaration } from '../manifest/types';\n\nfunction record(value: unknown): Record<string, unknown> | null {\n return typeof value === 'object' && value !== null\n ? (value as Record<string, unknown>)\n : null;\n}\n\nfunction list(value: unknown): unknown[] | null {\n return Array.isArray(value) ? value : null;\n}\n\nfunction isPoint(value: unknown): boolean {\n const pair = list(value);\n return (\n pair !== null &&\n pair.length === 2 &&\n typeof pair[0] === 'string' &&\n typeof pair[1] === 'number'\n );\n}\n\n/**\n * Checks a metrics response against the metrics the manifest declares and returns\n * what is wrong with it.\n *\n * @remarks\n * Returns an empty array when the response is sound, so a test asserts on an empty\n * list. It reports a body that is not `{ series: [...] }`, an entry with no\n * `metric` key, a metric the declarations do not name, a unit that disagrees with\n * the declared one, a missing `points` array, and any point that is not a\n * `[string, number]` pair. It does not check that every declared metric is\n * answered, so assert on the length yourself when that matters. The strings are the\n * ones `tappify extension doctor` reports.\n *\n * @example\n * ```ts\n * import { validateMetricsResponse } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * const metrics = [\n * { key: 'orders', label: 'Orders', unit: 'count' as const, kind: 'counter' as const },\n * ];\n *\n * it('answers orders as a counted series', () => {\n * const response = { series: [{ metric: 'orders', unit: 'count', points: [['2026-09-01', 12]] }] };\n * expect(validateMetricsResponse(response, metrics)).toEqual([]);\n * });\n * ```\n */\nexport function validateMetricsResponse(\n response: unknown,\n metrics: MetricDeclaration[],\n): string[] {\n const series = list(record(response)?.series);\n\n if (series === null) {\n return [\n 'A metrics response is { \"series\": [...] }. Return one entry per declared metric, even when it has no points.',\n ];\n }\n\n const issues: string[] = [];\n const declared = new Map(metrics.map(metric => [metric.key, metric]));\n\n for (const [index, entry] of series.entries()) {\n const item = record(entry);\n const key = typeof item?.metric === 'string' ? item.metric : null;\n\n if (item === null || key === null) {\n issues.push(`series[${String(index)}] has no \"metric\" key.`);\n continue;\n }\n\n const metric = declared.get(key);\n if (metric === undefined) {\n issues.push(\n `series[${String(index)}] reports \"${key}\", which is not declared under contributes.connector.metrics.`,\n );\n continue;\n }\n\n if (item.unit !== metric.unit) {\n issues.push(\n `series[${String(index)}] reports unit \"${String(item.unit)}\" but \"${key}\" is declared as \"${metric.unit}\".`,\n );\n }\n\n const points = list(item.points);\n if (points === null) {\n issues.push(`series[${String(index)}] has no \"points\" array.`);\n continue;\n }\n\n for (const [pointIndex, point] of points.entries()) {\n if (!isPoint(point)) {\n issues.push(\n `series[${String(index)}].points[${String(pointIndex)}] must be a two-element [timestamp, value] pair: a string and a number.`,\n );\n }\n }\n }\n\n return issues;\n}\n\n/**\n * Checks a tool's answer against the card shape its declared `returns` kind has to\n * carry, and returns what is missing.\n *\n * @remarks\n * Returns an empty array when the answer is sound. A `value` card needs `label` and\n * `value`, a `series` card `label` and `points`, a `table` card `columns` and\n * `rows`, a `list` card `items`, and a `comparison` card `label`, `left` and\n * `right`; anything that is not an object is reported on its own. It checks the\n * keys are present, not what they hold, so an empty `rows` array passes.\n *\n * @example\n * ```ts\n * import { validateToolResponse } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * it('answers keyword_gaps with a table card', () => {\n * const card = { columns: [{ key: 'term', label: 'Term' }], rows: [] };\n * expect(validateToolResponse(card, 'table')).toEqual([]);\n * });\n * ```\n */\nexport function validateToolResponse(\n value: unknown,\n returns: ToolDeclaration['returns'],\n): string[] {\n const body = record(value);\n if (body === null) {\n return [\n `A \"${returns}\" tool returns an object the host draws as a card, not ${typeof value}.`,\n ];\n }\n\n return CARD_KEYS[returns]\n .filter(key => !(key in body))\n .map(\n key =>\n `A \"${returns}\" tool response needs a \"${key}\" key; the host draws the card from it.`,\n );\n}\n","import type {\n WorkHydratedCommentCreatedEvent,\n WorkHydratedItemDeletedEvent,\n WorkHydratedStatusChangedEvent,\n} from '../server/work';\nimport {\n sendWorkEvent,\n type SendWorkEventOptions,\n type WorkEvent,\n} from '../server/work-events';\n\n/** A fresh complete comment signal with deterministic identity and shallow overrides. */\nexport function workCommentCreatedFixture(\n overrides: Partial<WorkHydratedCommentCreatedEvent> = {},\n): WorkHydratedCommentCreatedEvent {\n return {\n type: 'comment.created',\n externalItemId: 'item_test',\n providerEventId: 'event_comment_test',\n occurredAt: '2026-10-15T12:00:00.000Z',\n dedupeKey: 'item_test:event_comment_test',\n comment: {\n externalCommentId: 'comment_test',\n body: 'A provider comment.',\n author: { id: 'author_test', displayName: 'Sam' },\n createdAt: '2026-10-15T12:00:00.000Z',\n },\n ...overrides,\n };\n}\n\n/** A fresh complete status signal with deterministic identity and shallow overrides. */\nexport function workStatusChangedFixture(\n overrides: Partial<WorkHydratedStatusChangedEvent> = {},\n): WorkHydratedStatusChangedEvent {\n return {\n type: 'status.changed',\n externalItemId: 'item_test',\n providerEventId: 'event_status_test',\n occurredAt: '2026-10-15T12:00:00.000Z',\n dedupeKey: 'item_test:event_status_test',\n status: { id: 'resolved_test', label: 'Resolved', stage: 'done' },\n ...overrides,\n };\n}\n\n/** A fresh deletion signal with deterministic identity and shallow overrides. */\nexport function workItemDeletedFixture(\n overrides: Partial<WorkHydratedItemDeletedEvent> = {},\n): WorkHydratedItemDeletedEvent {\n return {\n type: 'item.deleted',\n externalItemId: 'item_test',\n providerEventId: 'event_deleted_test',\n occurredAt: '2026-10-15T12:00:00.000Z',\n dedupeKey: 'item_test:event_deleted_test',\n ...overrides,\n };\n}\n\n/**\n * Captures a signed work-event Request through the real sender without network I/O.\n *\n * @remarks\n * Uses the same validation, serialization, signing and callback URL as\n * sendWorkEvent. Supply the callback and secret your test install was configured\n * with; the accepting transport is supplied by this helper.\n */\nexport async function signedWorkEventRequest(\n event: WorkEvent,\n options: Omit<SendWorkEventOptions, 'fetch'>,\n): Promise<Request> {\n let request: Request | undefined;\n await sendWorkEvent(event, {\n ...options,\n fetch: (input, init) => {\n request = new Request(input, init);\n return Promise.resolve(new Response(null, { status: 202 }));\n },\n });\n if (!request)\n throw new Error('The work event sender did not produce a request.');\n return request;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/testing/render.tsx","../../src/testing/validators.ts"],"sourcesContent":["import { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport {\n render,\n type RenderOptions,\n type RenderResult,\n} from '@testing-library/react';\nimport type { ReactElement, ReactNode } from 'react';\nimport { installPrefix, procedurePrefix } from '../client/query';\nimport type { Tap } from '../client/types';\nimport { TapHostProvider } from '../host/provider';\nimport { createTapMock, type CreateTapMockOptions, type TapMock } from './mock';\n\n/**\n * What `renderWithTap` takes: its own three options, plus every\n * `CreateTapMockOptions` member it forwards to the mock.\n */\nexport interface RenderWithTapOptions extends CreateTapMockOptions {\n /** A mock you built yourself. Given, every mock option here is ignored. */\n mock?: TapMock;\n /** Your own client, for a test that reads the cache. Defaults to a fresh one. */\n queryClient?: QueryClient;\n /** Passed to Testing Library's `render`, for a mount point of your own. */\n container?: RenderOptions['container'];\n}\n\n/** What `renderWithTap` returns: Testing Library's result plus the bridge. */\nexport interface RenderWithTapResult extends RenderResult {\n /** The same object `useTap` returns inside the component. */\n tap: Tap;\n /** The mock behind the bridge, which the matchers take. */\n mock: TapMock;\n /** The client the render used, whether it was passed in or built here. */\n queryClient: QueryClient;\n}\n\n/**\n * Renders a component against the mock bridge, so a widget, tab, page, row action\n * or settings panel is testable outside Tappify.\n *\n * @remarks\n * It builds a mock from the same options `createTapMock` takes unless you pass\n * `mock`, wraps the component in the host provider and a `QueryClient` that does\n * not retry and does not cache between tests, and appends the mock's portal to the\n * rendered document so `TapDialog` has somewhere to go. It rewires\n * `tap.invalidate` onto that client, so a component calling it drops the right\n * cache entries. Scopes default to all of them, so pass `scopes` to cover the\n * refused path; a `handler` has to be built with `jwks: await testJwks()` and the\n * same `extensionId` the mock reports. Load\n * `@tappify/extension-sdk/testing/vitest` or `.../testing/jest` as a setup file\n * first: that installs the host CSS variables, registers the matchers and clears\n * the portal between tests.\n *\n * @example\n * ```tsx\n * import { TapStat, useTapQuery } from '@tappify/extension-sdk';\n * import { renderWithTap } from '@tappify/extension-sdk/testing';\n * import { screen, waitFor } from '@testing-library/react';\n * import { expect, it } from 'vitest';\n *\n * function Crashes() {\n * const crashes = useTapQuery({\n * kind: 'crashes',\n * range: { from: '2026-09-01', to: '2026-09-08' },\n * });\n * return <TapStat label=\"Issues\" value={crashes.data?.issues.length ?? 0} />;\n * }\n *\n * it('renders the issues the fixtures answer with', async () => {\n * renderWithTap(<Crashes />, { scopes: ['ui:render', 'crashes:read'] });\n * await waitFor(() => expect(screen.getByText('2')).toBeTruthy());\n * });\n * ```\n */\nexport function renderWithTap(\n ui: ReactElement,\n options: RenderWithTapOptions = {},\n): RenderWithTapResult {\n const mock = options.mock ?? createTapMock(options);\n const queryClient =\n options.queryClient ??\n new QueryClient({\n defaultOptions: { queries: { retry: false, gcTime: 0 } },\n });\n\n const installId = mock.tap.extension.installId;\n mock.tap.invalidate = name => {\n void queryClient.invalidateQueries({\n queryKey:\n name === undefined\n ? installPrefix(installId)\n : procedurePrefix(installId, name),\n });\n };\n\n function Wrapper({ children }: { children: ReactNode }): ReactElement {\n return (\n <QueryClientProvider client={queryClient}>\n <TapHostProvider tap={mock.tap}>{children}</TapHostProvider>\n </QueryClientProvider>\n );\n }\n\n const result = render(ui, { wrapper: Wrapper, container: options.container });\n result.baseElement.append(mock.portal);\n\n return { ...result, tap: mock.tap, mock, queryClient };\n}\n","import { CARD_KEYS } from '../manifest/constants';\nimport type { MetricDeclaration, ToolDeclaration } from '../manifest/types';\n\nfunction record(value: unknown): Record<string, unknown> | null {\n return typeof value === 'object' && value !== null\n ? (value as Record<string, unknown>)\n : null;\n}\n\nfunction list(value: unknown): unknown[] | null {\n return Array.isArray(value) ? value : null;\n}\n\nfunction isPoint(value: unknown): boolean {\n const pair = list(value);\n return (\n pair !== null &&\n pair.length === 2 &&\n typeof pair[0] === 'string' &&\n typeof pair[1] === 'number'\n );\n}\n\n/**\n * Checks a metrics response against the metrics the manifest declares and returns\n * what is wrong with it.\n *\n * @remarks\n * Returns an empty array when the response is sound, so a test asserts on an empty\n * list. It reports a body that is not `{ series: [...] }`, an entry with no\n * `metric` key, a metric the declarations do not name, a unit that disagrees with\n * the declared one, a missing `points` array, and any point that is not a\n * `[string, number]` pair. It does not check that every declared metric is\n * answered, so assert on the length yourself when that matters. The strings are the\n * ones `tappify extension doctor` reports.\n *\n * @example\n * ```ts\n * import { validateMetricsResponse } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * const metrics = [\n * { key: 'orders', label: 'Orders', unit: 'count' as const, kind: 'counter' as const },\n * ];\n *\n * it('answers orders as a counted series', () => {\n * const response = { series: [{ metric: 'orders', unit: 'count', points: [['2026-09-01', 12]] }] };\n * expect(validateMetricsResponse(response, metrics)).toEqual([]);\n * });\n * ```\n */\nexport function validateMetricsResponse(\n response: unknown,\n metrics: MetricDeclaration[],\n): string[] {\n const series = list(record(response)?.series);\n\n if (series === null) {\n return [\n 'A metrics response is { \"series\": [...] }. Return one entry per declared metric, even when it has no points.',\n ];\n }\n\n const issues: string[] = [];\n const declared = new Map(metrics.map(metric => [metric.key, metric]));\n\n for (const [index, entry] of series.entries()) {\n const item = record(entry);\n const key = typeof item?.metric === 'string' ? item.metric : null;\n\n if (item === null || key === null) {\n issues.push(`series[${String(index)}] has no \"metric\" key.`);\n continue;\n }\n\n const metric = declared.get(key);\n if (metric === undefined) {\n issues.push(\n `series[${String(index)}] reports \"${key}\", which is not declared under contributes.connector.metrics.`,\n );\n continue;\n }\n\n if (item.unit !== metric.unit) {\n issues.push(\n `series[${String(index)}] reports unit \"${String(item.unit)}\" but \"${key}\" is declared as \"${metric.unit}\".`,\n );\n }\n\n const points = list(item.points);\n if (points === null) {\n issues.push(`series[${String(index)}] has no \"points\" array.`);\n continue;\n }\n\n for (const [pointIndex, point] of points.entries()) {\n if (!isPoint(point)) {\n issues.push(\n `series[${String(index)}].points[${String(pointIndex)}] must be a two-element [timestamp, value] pair: a string and a number.`,\n );\n }\n }\n }\n\n return issues;\n}\n\n/**\n * Checks a tool's answer against the card shape its declared `returns` kind has to\n * carry, and returns what is missing.\n *\n * @remarks\n * Returns an empty array when the answer is sound. A `value` card needs `label` and\n * `value`, a `series` card `label` and `points`, a `table` card `columns` and\n * `rows`, a `list` card `items`, and a `comparison` card `label`, `left` and\n * `right`; anything that is not an object is reported on its own. It checks the\n * keys are present, not what they hold, so an empty `rows` array passes.\n *\n * @example\n * ```ts\n * import { validateToolResponse } from '@tappify/extension-sdk/testing';\n * import { expect, it } from 'vitest';\n *\n * it('answers keyword_gaps with a table card', () => {\n * const card = { columns: [{ key: 'term', label: 'Term' }], rows: [] };\n * expect(validateToolResponse(card, 'table')).toEqual([]);\n * });\n * ```\n */\nexport function validateToolResponse(\n value: unknown,\n returns: ToolDeclaration['returns'],\n): string[] {\n const body = record(value);\n if (body === null) {\n return [\n `A \"${returns}\" tool returns an object the host draws as a card, not ${typeof value}.`,\n ];\n }\n\n return CARD_KEYS[returns]\n .filter(key => !(key in body))\n .map(\n key =>\n `A \"${returns}\" tool response needs a \"${key}\" key; the host draws the card from it.`,\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/testing/jest.cjs
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
var
|
|
6
|
-
require('../chunk-
|
|
5
|
+
var _chunk3YWVYA42cjs = require('../chunk-3YWVYA42.cjs');
|
|
6
|
+
require('../chunk-2DBSVKAU.cjs');
|
|
7
7
|
require('../chunk-Q7SFCCGT.cjs');
|
|
8
8
|
|
|
9
9
|
// src/testing/jest.ts
|
|
10
10
|
var jestGlobal = globalThis;
|
|
11
|
-
|
|
11
|
+
_chunk3YWVYA42cjs.installHostTheme.call(void 0, document.documentElement);
|
|
12
12
|
if (jestGlobal.expect !== void 0) {
|
|
13
|
-
|
|
13
|
+
_chunk3YWVYA42cjs.registerTapMatchers.call(void 0, jestGlobal.expect);
|
|
14
14
|
}
|
|
15
15
|
_optionalChain([jestGlobal, 'access', _ => _.afterEach, 'optionalCall', _2 => _2(() => {
|
|
16
|
-
|
|
16
|
+
_chunk3YWVYA42cjs.clearTapPortals.call(void 0, );
|
|
17
17
|
})]);
|
|
18
18
|
//# sourceMappingURL=jest.cjs.map
|
package/dist/testing/jest.mjs
CHANGED
|
@@ -2,8 +2,8 @@ import {
|
|
|
2
2
|
clearTapPortals,
|
|
3
3
|
installHostTheme,
|
|
4
4
|
registerTapMatchers
|
|
5
|
-
} from "../chunk-
|
|
6
|
-
import "../chunk-
|
|
5
|
+
} from "../chunk-IGK2AP6J.mjs";
|
|
6
|
+
import "../chunk-CSTYBKZC.mjs";
|
|
7
7
|
import "../chunk-J5LGTIGS.mjs";
|
|
8
8
|
|
|
9
9
|
// src/testing/jest.ts
|
package/dist/testing/vitest.cjs
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
var
|
|
6
|
-
require('../chunk-
|
|
5
|
+
var _chunk3YWVYA42cjs = require('../chunk-3YWVYA42.cjs');
|
|
6
|
+
require('../chunk-2DBSVKAU.cjs');
|
|
7
7
|
require('../chunk-Q7SFCCGT.cjs');
|
|
8
8
|
|
|
9
9
|
// src/testing/vitest.ts
|
|
10
10
|
var _vitest = require('vitest');
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
_chunk3YWVYA42cjs.installHostTheme.call(void 0, document.documentElement);
|
|
12
|
+
_chunk3YWVYA42cjs.registerTapMatchers.call(void 0, _vitest.expect);
|
|
13
13
|
_vitest.afterEach.call(void 0, () => {
|
|
14
|
-
|
|
14
|
+
_chunk3YWVYA42cjs.clearTapPortals.call(void 0, );
|
|
15
15
|
});
|
|
16
16
|
//# sourceMappingURL=vitest.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/testing/vitest.cjs","../../src/testing/vitest.ts"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACA;ACRA,gCAAkC;
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/extension-sdk/extension-sdk/dist/testing/vitest.cjs","../../src/testing/vitest.ts"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACA;ACRA,gCAAkC;AAiBlC,gDAAA,QAAiB,CAAS,eAAe,CAAA;AACzC,mDAAA,cAA0B,CAAA;AAE1B,+BAAA,CAAU,EAAA,GAAM;AACd,EAAA,+CAAA,CAAgB;AAClB,CAAC,CAAA","file":"/home/runner/work/extension-sdk/extension-sdk/dist/testing/vitest.cjs","sourcesContent":[null,"import { afterEach, expect } from 'vitest';\nimport { clearTapPortals, installHostTheme } from './environment';\nimport { registerTapMatchers } from './matchers';\n\nexport type { MatcherResult } from './matchers';\n\n/* eslint-disable @typescript-eslint/no-explicit-any -- an augmentation has to repeat vitest's own `Matchers<T = any>` parameter list verbatim */\ndeclare module 'vitest' {\n interface Matchers<T = any> {\n toHaveNavigatedTo(path: string): T;\n toHaveToasted(expected: string | RegExp): T;\n toHaveRunAction(actionId: string, input?: unknown): T;\n toHaveStored(collection: string, document: unknown): T;\n }\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\ninstallHostTheme(document.documentElement);\nregisterTapMatchers(expect);\n\nafterEach(() => {\n clearTapPortals();\n});\n"]}
|
|
@@ -8,13 +8,3 @@ declare module 'vitest' {
|
|
|
8
8
|
toHaveStored(collection: string, document: unknown): T;
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
-
declare global {
|
|
12
|
-
namespace Chai {
|
|
13
|
-
interface Assertion {
|
|
14
|
-
toHaveNavigatedTo(path: string): void;
|
|
15
|
-
toHaveToasted(expected: string | RegExp): void;
|
|
16
|
-
toHaveRunAction(actionId: string, input?: unknown): void;
|
|
17
|
-
toHaveStored(collection: string, document: unknown): void;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
package/dist/testing/vitest.d.ts
CHANGED
|
@@ -8,13 +8,3 @@ declare module 'vitest' {
|
|
|
8
8
|
toHaveStored(collection: string, document: unknown): T;
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
-
declare global {
|
|
12
|
-
namespace Chai {
|
|
13
|
-
interface Assertion {
|
|
14
|
-
toHaveNavigatedTo(path: string): void;
|
|
15
|
-
toHaveToasted(expected: string | RegExp): void;
|
|
16
|
-
toHaveRunAction(actionId: string, input?: unknown): void;
|
|
17
|
-
toHaveStored(collection: string, document: unknown): void;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
package/dist/testing/vitest.mjs
CHANGED
|
@@ -2,8 +2,8 @@ import {
|
|
|
2
2
|
clearTapPortals,
|
|
3
3
|
installHostTheme,
|
|
4
4
|
registerTapMatchers
|
|
5
|
-
} from "../chunk-
|
|
6
|
-
import "../chunk-
|
|
5
|
+
} from "../chunk-IGK2AP6J.mjs";
|
|
6
|
+
import "../chunk-CSTYBKZC.mjs";
|
|
7
7
|
import "../chunk-J5LGTIGS.mjs";
|
|
8
8
|
|
|
9
9
|
// src/testing/vitest.ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/testing/vitest.ts"],"sourcesContent":["import { afterEach, expect } from 'vitest';\nimport { clearTapPortals, installHostTheme } from './environment';\nimport { registerTapMatchers } from './matchers';\n\nexport type { MatcherResult } from './matchers';\n\n/* eslint-disable @typescript-eslint/no-explicit-any -- an augmentation has to repeat vitest's own `Matchers<T = any>` parameter list verbatim */\ndeclare module 'vitest' {\n interface Matchers<T = any> {\n toHaveNavigatedTo(path: string): T;\n toHaveToasted(expected: string | RegExp): T;\n toHaveRunAction(actionId: string, input?: unknown): T;\n toHaveStored(collection: string, document: unknown): T;\n }\n}\n
|
|
1
|
+
{"version":3,"sources":["../../src/testing/vitest.ts"],"sourcesContent":["import { afterEach, expect } from 'vitest';\nimport { clearTapPortals, installHostTheme } from './environment';\nimport { registerTapMatchers } from './matchers';\n\nexport type { MatcherResult } from './matchers';\n\n/* eslint-disable @typescript-eslint/no-explicit-any -- an augmentation has to repeat vitest's own `Matchers<T = any>` parameter list verbatim */\ndeclare module 'vitest' {\n interface Matchers<T = any> {\n toHaveNavigatedTo(path: string): T;\n toHaveToasted(expected: string | RegExp): T;\n toHaveRunAction(actionId: string, input?: unknown): T;\n toHaveStored(collection: string, document: unknown): T;\n }\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\ninstallHostTheme(document.documentElement);\nregisterTapMatchers(expect);\n\nafterEach(() => {\n clearTapPortals();\n});\n"],"mappings":";;;;;;;;;AAAA,SAAS,WAAW,cAAc;AAiBlC,iBAAiB,SAAS,eAAe;AACzC,oBAAoB,MAAM;AAE1B,UAAU,MAAM;AACd,kBAAgB;AAClB,CAAC;","names":[]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as ScopeKey, H as HostEventName } from './constants-
|
|
1
|
+
import { S as ScopeKey, H as HostEventName } from './constants-DJ_KgZpN.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The storage collections the manifest declares, keyed by collection name, which
|
|
@@ -556,12 +556,11 @@ interface TapInternals {
|
|
|
556
556
|
*
|
|
557
557
|
* @example
|
|
558
558
|
* ```tsx
|
|
559
|
-
* import { useTap } from '@tappify/extension-sdk';
|
|
560
|
-
* import { Metric } from '@tappify/extension-sdk/ui';
|
|
559
|
+
* import { TapStat, useTap } from '@tappify/extension-sdk';
|
|
561
560
|
*
|
|
562
561
|
* function Installs({ count }: { count: number }) {
|
|
563
562
|
* const tap = useTap();
|
|
564
|
-
* return <
|
|
563
|
+
* return <TapStat label="Installs" value={tap.format.number(count)} />;
|
|
565
564
|
* }
|
|
566
565
|
* ```
|
|
567
566
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as ScopeKey, H as HostEventName } from './constants-
|
|
1
|
+
import { S as ScopeKey, H as HostEventName } from './constants-DJ_KgZpN.cjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The storage collections the manifest declares, keyed by collection name, which
|
|
@@ -556,12 +556,11 @@ interface TapInternals {
|
|
|
556
556
|
*
|
|
557
557
|
* @example
|
|
558
558
|
* ```tsx
|
|
559
|
-
* import { useTap } from '@tappify/extension-sdk';
|
|
560
|
-
* import { Metric } from '@tappify/extension-sdk/ui';
|
|
559
|
+
* import { TapStat, useTap } from '@tappify/extension-sdk';
|
|
561
560
|
*
|
|
562
561
|
* function Installs({ count }: { count: number }) {
|
|
563
562
|
* const tap = useTap();
|
|
564
|
-
* return <
|
|
563
|
+
* return <TapStat label="Installs" value={tap.format.number(count)} />;
|
|
565
564
|
* }
|
|
566
565
|
* ```
|
|
567
566
|
*/
|