@sentientui/react 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,118 @@
1
+ import { ReactElement } from 'react';
2
+ import { RenderOptions, RenderResult } from '@testing-library/react';
3
+ import { RequestHandler } from 'msw';
4
+
5
+ type ScenarioWeight = {
6
+ variantId: string;
7
+ pulls: number;
8
+ avgReward: number;
9
+ };
10
+ type ScenarioApiOverride = 'error' | number | {
11
+ status?: number;
12
+ body?: unknown;
13
+ delayMs?: number;
14
+ };
15
+ type SentientScenario = {
16
+ variants?: Record<string, string>;
17
+ layout?: string[];
18
+ persona?: string;
19
+ weights?: Record<string, ScenarioWeight[]>;
20
+ api?: Record<string, ScenarioApiOverride>;
21
+ };
22
+ /** Apply a scenario by setting the client-forcing globals the SDK reads. */
23
+ declare function applyScenario(scenario?: SentientScenario): void;
24
+ /** Clear all forced state. */
25
+ declare function resetScenario(): void;
26
+
27
+ /** Turn a scenario into MSW handlers stubbing every SDK endpoint + capturing events. */
28
+ declare function scenarioToHandlers(scenario?: SentientScenario): RequestHandler[];
29
+
30
+ /** A framework-agnostic resolved response. `json` undefined ⇒ empty body. */
31
+ type ResolvedResponse = {
32
+ status: number;
33
+ json?: unknown;
34
+ };
35
+ /**
36
+ * Resolve a request against a scenario. Returns a response, or null for routes
37
+ * outside `/v1/*` (let the caller pass through). Shared by the MSW handlers and
38
+ * the Playwright/Cypress adapters so behaviour can't drift.
39
+ */
40
+ declare function resolveScenario(scenario: SentientScenario, _method: string, url: string, bodyText: string | null): Promise<ResolvedResponse | null>;
41
+
42
+ type CapturedEvent = {
43
+ eventType: string;
44
+ goalType?: string;
45
+ componentId?: string;
46
+ variantId?: string;
47
+ [k: string]: unknown;
48
+ };
49
+ declare function getSentientEvents(): CapturedEvent[];
50
+ declare function clearSentientEvents(): void;
51
+ /** True if any captured event is a goal (component goal_achieved or named goal) with this name. */
52
+ declare function hasFiredGoal(events: CapturedEvent[], goalName: string): boolean;
53
+
54
+ type InitData = {
55
+ overrides: Record<string, string>;
56
+ layout: string[] | null;
57
+ };
58
+ type PwRoute = {
59
+ request(): {
60
+ method(): string;
61
+ url(): string;
62
+ postData(): string | null;
63
+ };
64
+ fulfill(r: {
65
+ status: number;
66
+ contentType?: string;
67
+ body?: string;
68
+ }): Promise<void>;
69
+ continue(): Promise<void>;
70
+ };
71
+ /** Structural subset of Playwright's `Page` — avoids a hard dependency on @playwright/test. */
72
+ type PwPage = {
73
+ addInitScript(script: (arg: InitData) => void, arg: InitData): Promise<void>;
74
+ route(url: string, handler: (route: PwRoute) => unknown): Promise<void>;
75
+ };
76
+ /**
77
+ * Make a Playwright `page` serve a SentientUI scenario: forces variants/layout
78
+ * before load and stubs every `/v1/*` request from the scenario, capturing
79
+ * posted events. Returns a handle exposing `.events()`.
80
+ */
81
+ declare function mockSentient(page: PwPage, scenario?: SentientScenario): Promise<{
82
+ events: () => CapturedEvent[];
83
+ }>;
84
+
85
+ type CyReq = {
86
+ method: string;
87
+ url: string;
88
+ body: unknown;
89
+ reply(r: {
90
+ statusCode: number;
91
+ body?: unknown;
92
+ }): void;
93
+ };
94
+ /** Structural subset of Cypress's `cy` — avoids a hard dependency on cypress. */
95
+ type Cy = {
96
+ intercept(url: string, handler: (req: CyReq) => void | Promise<void>): unknown;
97
+ on(event: string, cb: (win: Record<string, unknown>) => void): unknown;
98
+ };
99
+ /**
100
+ * Make Cypress serve a SentientUI scenario: forces variants/layout on the app
101
+ * window before load and stubs every `/v1/*` request from the scenario. Call in
102
+ * a `beforeEach` before `cy.visit`.
103
+ *
104
+ * Note: for event assertions in Cypress, alias the intercept (`cy.intercept(...).as('ev')`)
105
+ * and `cy.wait('@ev')` — captured module state does not cross the browser/Node boundary.
106
+ */
107
+ declare function mockSentientCypress(cy: Cy, scenario?: SentientScenario): void;
108
+
109
+ /**
110
+ * Render `ui` under a SentientUI provider configured for tests: consent is off,
111
+ * so the SDK never initialises a client and every <Adaptive> renders its control
112
+ * variant with zero network. A scenario forces specific variants/layout.
113
+ */
114
+ declare function renderWithSentient(ui: ReactElement, scenario?: SentientScenario, options?: RenderOptions): RenderResult;
115
+ /** Call in a test setup file to reset forced state after each test. */
116
+ declare function setupSentientTests(): void;
117
+
118
+ export { type CapturedEvent, type ResolvedResponse, type ScenarioApiOverride, type ScenarioWeight, type SentientScenario, applyScenario, clearSentientEvents, getSentientEvents, hasFiredGoal, mockSentient, mockSentientCypress, renderWithSentient, resetScenario, resolveScenario, scenarioToHandlers, setupSentientTests };
@@ -0,0 +1,3 @@
1
+ 'use client';
2
+ "use strict";var ie=Object.create;var x=Object.defineProperty,re=Object.defineProperties,oe=Object.getOwnPropertyDescriptor,se=Object.getOwnPropertyDescriptors,ae=Object.getOwnPropertyNames,L=Object.getOwnPropertySymbols,ue=Object.getPrototypeOf,M=Object.prototype.hasOwnProperty,de=Object.prototype.propertyIsEnumerable;var N=(e,t,n)=>t in e?x(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,k=(e,t)=>{for(var n in t||(t={}))M.call(t,n)&&N(e,n,t[n]);if(L)for(var n of L(t))de.call(t,n)&&N(e,n,t[n]);return e},G=(e,t)=>re(e,se(t));var le=(e,t)=>{for(var n in t)x(e,n,{get:t[n],enumerable:!0})},V=(e,t,n,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ae(t))!M.call(e,o)&&o!==n&&x(e,o,{get:()=>t[o],enumerable:!(i=oe(t,o))||i.enumerable});return e};var ce=(e,t,n)=>(n=e!=null?ie(ue(e)):{},V(t||!e||!e.__esModule?x(n,"default",{value:e,enumerable:!0}):n,e)),pe=e=>V(x({},"__esModule",{value:!0}),e);var be={};le(be,{applyScenario:()=>I,clearSentientEvents:()=>X,getSentientEvents:()=>R,hasFiredGoal:()=>q,mockSentient:()=>Y,mockSentientCypress:()=>Z,renderWithSentient:()=>_e,resetScenario:()=>O,resolveScenario:()=>m,scenarioToHandlers:()=>Q,setupSentientTests:()=>he});module.exports=pe(be);var ee=require("@testing-library/react");var d=require("react"),S=require("@sentientui/core");var ve=new Map,ge=new Map;function H(e,t){ve.set(e,t);let n=ge.get(e);if(n)for(let i of n)try{i(t)}catch(o){}}var fe=!1,B=new Set;function P(){return fe}function J(e){return B.add(e),()=>{B.delete(e)}}function $(e){return{track:()=>{},goal:()=>{},componentGoal:()=>{},identify:()=>{},fetchWeights:()=>Promise.resolve([]),getAssignment:(t,n)=>e.getAssignment(t,n),assign:(t,n,i,o)=>e.assign(t,n,i,o),getGraph:()=>e.getGraph(),destroy:()=>e.destroy()}}var U=require("react/jsx-runtime");function me(){var e,t;if(typeof window=="undefined")return"desktop:direct";try{let n=(0,S.detectDeviceClass)((e=navigator.userAgent)!=null?e:""),i=(0,S.detectTrafficSource)((t=document.referrer)!=null?t:"",window.location.origin);return`${n}:${i}`}catch(n){return"desktop:direct"}}var ye=(0,d.createContext)({client:null,apiKey:"",initialAssignments:{},sessionSegment:"desktop:direct",ssrFallback:"first",onAssignment:void 0,initialLayoutOrder:null});function z(e){var w;let[t,n]=(0,d.useState)(null),[i]=(0,d.useState)(()=>{var u;return(u=e.sessionSegment)!=null?u:me()}),[o,r]=(0,d.useState)(P());(0,d.useEffect)(()=>J(()=>r(P())),[]),(0,d.useEffect)(()=>{if(e.consent===!1&&!e.preConsentBehavior){n(p=>(p==null||p.destroy(),null));return}let u={apiKey:e.apiKey,context:e.context,debug:e.debug,initialAssignments:e.initialAssignments,sessionSegment:i,consent:e.consent,preConsentBehavior:e.preConsentBehavior,respectDoNotTrack:e.respectDoNotTrack,ssrSessionId:e.ssrSessionId,country:e.country},c=!1,l=null;return e.enableGraph?import("@sentientui/core/graph").then(({init:p})=>{c||(l=p(G(k({},u),{graph:!0})),n(l))}):(l=(0,S.init)(u),n(l)),()=>{c=!0,l==null||l.destroy()}},[e.consent]),(0,d.useEffect)(()=>{if(!t)return;let u=!1,c=async()=>{if(u)return;let p;try{p=await t.fetchWeights()}catch(f){return}if(!u)for(let f of p){let A={componentId:f.componentId,updatedAt:f.updatedAt,variants:f.variants.map(y=>{var _;return{variantId:y.variantId,pulls:y.pulls,avgReward:(_=y.avgReward)!=null?_:0}})};H(f.componentId,A)}};c();let l=setInterval(()=>{c()},6e4);return()=>{u=!0,clearInterval(l)}},[t]);let s=(w=e.ssrFallback)!=null?w:"first",a=(0,d.useMemo)(()=>t&&o?$(t):t,[t,o]),v=(0,d.useMemo)(()=>{var u,c;return{client:a,apiKey:e.apiKey,initialAssignments:(u=e.initialAssignments)!=null?u:{},sessionSegment:i,ssrFallback:s,onAssignment:e.onAssignment,initialLayoutOrder:(c=e.initialLayoutOrder)!=null?c:null}},[a,e.apiKey,e.initialAssignments,i,s,e.onAssignment,e.initialLayoutOrder]);return(0,U.jsx)(ye.Provider,{value:v,children:e.children})}function I(e={}){var n;let t=window;t.__sentient_overrides=k({},(n=e.variants)!=null?n:{}),e.layout?t.__sentient_layout_override=e.layout:delete t.__sentient_layout_override}function O(){let e=window;delete e.__sentient_overrides,delete e.__sentient_layout_override}var C=require("msw");var E=[];function W(e){E.push(e)}function R(){return[...E]}function X(){E.length=0}function q(e,t){return e.some(n=>(n.eventType==="goal_achieved"||n.eventType==="goal")&&n.goalType===t)}function Se(e){var t;try{return new URL(e).pathname}catch(n){return(t=e.split("?")[0])!=null?t:e}}async function we(e,t){var i,o,r;let n=(i=e.api)==null?void 0:i[t];return n===void 0?null:n==="error"?{status:500}:typeof n=="number"?{status:n}:(n.delayMs&&await new Promise(s=>setTimeout(s,n.delayMs)),{status:(o=n.status)!=null?o:200,json:(r=n.body)!=null?r:{}})}async function m(e,t,n,i){var v,w,u,c,l,p,f,A,y,_,j,T,D,F,K;let o=Se(n);if(!o.includes("/v1/"))return null;let r="/v1/"+((v=o.split("/v1/")[1])!=null?v:""),s=await we(e,r);if(s)return s;let a=i?JSON.parse(i):{};if(r==="/v1/sessions")return{status:204};if(r==="/v1/events"){for(let g of a)W(g);return{status:204}}if(r==="/v1/goals")return W({eventType:"goal",goalType:a.name}),{status:204};if(r==="/v1/assign"){let g=a;return{status:200,json:{variantId:(l=(c=(w=e.variants)==null?void 0:w[g.componentId])!=null?c:(u=g.variantIds)==null?void 0:u[0])!=null?l:"control",assignmentTtlMs:6e4}}}if(r==="/v1/decide"){let g=a;return{status:200,json:{layoutOrder:(f=e.layout)!=null?f:((p=g.sections)!=null?p:[]).map(b=>b.id),assignments:(A=e.variants)!=null?A:{},persona:(y=e.persona)!=null?y:"unknown",confidence:1}}}if(r==="/v1/explain"){let g=a,h=(j=e.layout)!=null?j:((_=g.sections)!=null?_:[]).map(ne=>ne.id),b=(D=(T=g.persona)!=null?T:e.persona)!=null?D:"unknown";return{status:200,json:{layoutOrder:h,assignments:(F=e.variants)!=null?F:{},persona:b,reasons:[]}}}return r==="/v1/weights"?{status:200,json:{components:Object.entries((K=e.weights)!=null?K:{}).map(([h,b])=>({componentId:h,updatedAt:0,variants:b}))}}:null}function Q(e={}){return[C.http.all("*/v1/*",async({request:t})=>{let n=t.method==="GET"||t.method==="HEAD"?null:await t.text(),i=await m(e,t.method,t.url,n);if(i)return i.json===void 0?new C.HttpResponse(null,{status:i.status}):C.HttpResponse.json(i.json,{status:i.status})})]}async function Y(e,t={}){var i,o;let n={overrides:(i=t.variants)!=null?i:{},layout:(o=t.layout)!=null?o:null};return await e.addInitScript(r=>{window.__sentient_overrides=r.overrides,r.layout&&(window.__sentient_layout_override=r.layout)},n),await e.route("**/v1/**",async r=>{var v;let s=r.request(),a=await m(t,s.method(),s.url(),s.postData());if(!a)return r.continue();await r.fulfill({status:a.status,contentType:"application/json",body:JSON.stringify((v=a.json)!=null?v:{})})}),{events:()=>R()}}function Z(e,t={}){var o,r;let n=(o=t.variants)!=null?o:{},i=(r=t.layout)!=null?r:null;e.on("window:before:load",s=>{s.__sentient_overrides=n,i&&(s.__sentient_layout_override=i)}),e.intercept("**/v1/**",async s=>{var v;let a=await m(t,s.method,s.url,s.body!=null?JSON.stringify(s.body):null);a&&s.reply({statusCode:a.status,body:(v=a.json)!=null?v:""})})}var te=require("react/jsx-runtime");function _e(e,t={},n){return I(t),(0,ee.render)((0,te.jsx)(z,{apiKey:"pk_test",context:"saas",consent:!1,children:e}),n)}function he(){var t;let e=globalThis;(t=e.afterEach)==null||t.call(e,()=>O())}0&&(module.exports={applyScenario,clearSentientEvents,getSentientEvents,hasFiredGoal,mockSentient,mockSentientCypress,renderWithSentient,resetScenario,resolveScenario,scenarioToHandlers,setupSentientTests});
3
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/testing/index.tsx","../src/provider.tsx","../src/weights-store.ts","../src/preview-mode.ts","../src/testing/scenario.ts","../src/testing/handlers.ts","../src/testing/events.ts","../src/testing/resolve.ts","../src/testing/playwright.ts","../src/testing/cypress.ts"],"sourcesContent":["import type { ReactElement } from 'react';\nimport { render, type RenderOptions, type RenderResult } from '@testing-library/react';\nimport { AdaptiveProvider } from '../provider.js';\nimport { applyScenario, resetScenario, type SentientScenario } from './scenario.js';\n\nexport type { SentientScenario };\nexport { applyScenario, resetScenario };\nexport { scenarioToHandlers } from './handlers.js';\nexport { resolveScenario, type ResolvedResponse } from './resolve.js';\nexport { getSentientEvents, clearSentientEvents, hasFiredGoal, type CapturedEvent } from './events.js';\nexport type { ScenarioWeight, ScenarioApiOverride } from './scenario.js';\nexport { mockSentient } from './playwright.js';\nexport { mockSentientCypress } from './cypress.js';\n\n/**\n * Render `ui` under a SentientUI provider configured for tests: consent is off,\n * so the SDK never initialises a client and every <Adaptive> renders its control\n * variant with zero network. A scenario forces specific variants/layout.\n */\nexport function renderWithSentient(\n ui: ReactElement,\n scenario: SentientScenario = {},\n options?: RenderOptions,\n): RenderResult {\n applyScenario(scenario);\n return render(\n <AdaptiveProvider apiKey=\"pk_test\" context=\"saas\" consent={false}>\n {ui}\n </AdaptiveProvider>,\n options,\n );\n}\n\n/** Call in a test setup file to reset forced state after each test. */\nexport function setupSentientTests(): void {\n const g = globalThis as unknown as { afterEach?: (fn: () => void) => void };\n g.afterEach?.(() => resetScenario());\n}\n","'use client';\n\ndeclare const process: { env?: { NODE_ENV?: string } } | undefined;\n\nimport { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';\nimport {\n detectDeviceClass,\n detectTrafficSource,\n init,\n type SentientClient,\n type SentientConfig,\n} from '@sentientui/core';\nimport { update as updateWeightsStore, type ComponentWeights } from './weights-store.js';\nimport { getPreviewMode, subscribePreview, createPreviewClient } from './preview-mode.js';\n\n/**\n * Mirrors the segment derivation inside core `init()` so the cache key used\n * by `useAssignment` always matches the key `assign()` writes under. Before\n * this, the context defaulted to 'desktop:direct' while core used the\n * detected segment — a systematic cache miss for every integration that\n * didn't pass `sessionSegment` explicitly.\n */\nfunction deriveDefaultSegment(): string {\n if (typeof window === 'undefined') return 'desktop:direct';\n try {\n const device = detectDeviceClass(navigator.userAgent ?? '');\n const source = detectTrafficSource(document.referrer ?? '', window.location.origin);\n return `${device}:${source}`;\n } catch {\n return 'desktop:direct';\n }\n}\n\n/** How to render adaptive slots during SSR when assignments are not preloaded. */\nexport type SsrFallback = 'first' | 'none';\n\ntype AdaptiveContextValue = {\n client: SentientClient | null;\n // The publishable API key (pk_…). Historically named `projectId` because the\n // API uses it as the project identifier on the wire, but it is the API key,\n // not the project UUID. Field renamed for clarity.\n apiKey: string;\n initialAssignments: Record<string, string>;\n sessionSegment: string;\n ssrFallback: SsrFallback;\n onAssignment: ((componentId: string, variantId: string) => void) | undefined;\n initialLayoutOrder: string[] | null;\n};\n\nconst AdaptiveContext = createContext<AdaptiveContextValue>({\n client: null,\n apiKey: '',\n initialAssignments: {},\n sessionSegment: 'desktop:direct',\n ssrFallback: 'first',\n onAssignment: undefined,\n initialLayoutOrder: null,\n});\n\nexport type AdaptiveProviderProps = {\n apiKey: string;\n context: SentientConfig['context'];\n debug?: boolean;\n /**\n * SSR-preloaded assignments from `preloadAssignments()` / `loadAdaptiveAssignments()`.\n * Passed through to `useAssignment` as synchronous initial state so crawlers and\n * the first paint see real content (recommended for SEO).\n */\n initialAssignments?: Record<string, string>;\n /**\n * Bandit segment from SSR (`device:source`). Keeps cache, assign, and worker\n * weights on one row — must match `loadAdaptiveAssignments` / session upsert.\n */\n sessionSegment?: string;\n /**\n * When no `initialAssignments` exist for a component, `'first'` renders\n * `variantIds[0]` in server HTML (safe default for SEO). Use `'none'` only for\n * decorative slots marked `clientOnly`.\n * @default 'first'\n */\n ssrFallback?: SsrFallback;\n /**\n * Consent gate. When `false` the SDK is not initialised and no events are\n * sent. Flip to `true` (e.g. after the user accepts the cookie banner) to\n * initialise and begin tracking.\n */\n consent?: boolean;\n /**\n * Behavior before consent is granted. Pass `'statistical_winner'` to serve the\n * best-performing variant via `GET /v1/winner` with zero tracking while the\n * consent banner is showing. Requires `consent: false`.\n * @see SentientConfig.preConsentBehavior\n */\n preConsentBehavior?: 'statistical_winner' | 'control';\n /**\n * Honor the browser's Do Not Track signal. Defaults to `true`: when DNT is\n * enabled the SDK sets no cookies and sends no tracking data (overriding\n * `consent: true`). Set `false` to make your own consent gate authoritative.\n * @see SentientConfig.respectDoNotTrack\n */\n respectDoNotTrack?: boolean;\n /**\n * Called once per component the first time a variant is resolved for that\n * component in this session. Use to forward assignments to your own analytics\n * (Mixpanel, PostHog, Segment, etc.) without having to wrap `useAssignment`.\n */\n onAssignment?: (componentId: string, variantId: string) => void;\n /**\n * SSR-preloaded section order from `loadAdaptiveDecision()`.\n * Pass the `layoutOrder` field from `DecideResult`. When set,\n * `useLayoutOrder()` returns this on first render so there is no layout shift.\n */\n initialLayoutOrder?: string[] | null;\n /**\n * Session ID generated during SSR (the `sessionId` field returned by\n * `loadAdaptiveAssignments` / `loadAdaptiveDecision`). When provided and no\n * existing session cookie or localStorage entry is found, the client adopts\n * this ID so events and goals are attributed to the same session the server\n * used for variant assignment.\n */\n ssrSessionId?: string;\n /**\n * ISO 3166-1 alpha-2 country code. Pass the value of the `CF-IPCountry`\n * header from your Next.js server component to populate country on landing\n * sessions without client-side geo lookup.\n */\n country?: string;\n /**\n * Enable DOM graph scanning + page-structure sync. When `true`, the provider\n * dynamically loads `@sentientui/core/graph` and uses its graph-capable\n * `init()` for the single client, so the SDK scans your page structure and\n * syncs it to power the dashboard graph page. Left off (default), the lean\n * bundle is used and the graph entry is never loaded.\n */\n enableGraph?: boolean;\n children: ReactNode;\n};\n\n/**\n * Initialises the Sentient core SDK in a useEffect (SSR-safe) and exposes the\n * client via React context. Re-initialises when `consent` changes.\n */\nexport function AdaptiveProvider(props: AdaptiveProviderProps): JSX.Element {\n const [client, setClient] = useState<SentientClient | null>(null);\n // Derived once per mount: identical to what core init() computes, so cache\n // reads (context segment) and cache writes (core segment) always agree.\n const [sessionSegment] = useState(() => props.sessionSegment ?? deriveDefaultSegment());\n // Devtools preview: when on, expose an event-suppressing client so previewing\n // variants/personas writes nothing. Off by default (inert in production).\n const [previewOn, setPreviewOn] = useState(getPreviewMode());\n useEffect(() => subscribePreview(() => setPreviewOn(getPreviewMode())), []);\n\n useEffect(() => {\n // When consent is explicitly false with no preConsentBehavior, tear down any existing client.\n if (props.consent === false && !props.preConsentBehavior) {\n setClient((prev: SentientClient | null) => {\n prev?.destroy();\n return null;\n });\n return;\n }\n\n const config = {\n apiKey: props.apiKey,\n context: props.context,\n debug: props.debug,\n initialAssignments: props.initialAssignments,\n sessionSegment,\n consent: props.consent,\n preConsentBehavior: props.preConsentBehavior,\n respectDoNotTrack: props.respectDoNotTrack,\n ssrSessionId: props.ssrSessionId,\n country: props.country,\n };\n\n // Track the client created by this effect run so cleanup destroys exactly\n // the right one, and so a late-resolving dynamic import can bail if the\n // effect was already torn down (unmount / consent change).\n let cancelled = false;\n let created: SentientClient | null = null;\n\n if (props.enableGraph) {\n // Load the graph entry only when asked — keeps the DOM scanner out of the\n // lean bundle. The provider still creates ONE client (graph-capable).\n void import('@sentientui/core/graph').then(({ init: initGraph }) => {\n if (cancelled) return;\n created = initGraph({ ...config, graph: true });\n setClient(created);\n });\n } else {\n created = init(config);\n setClient(created);\n }\n\n return () => {\n cancelled = true;\n created?.destroy();\n };\n // Re-init when consent changes. Other props (incl. enableGraph) are\n // intentionally stable for a session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [props.consent]);\n\n // Poll /v1/weights every 60 s so long-lived sessions see updated bandit weights\n // without a page reload. useAssignment subscribers react via weights-store.\n useEffect(() => {\n if (!client) return;\n let cancelled = false;\n const poll = async (): Promise<void> => {\n if (cancelled) return;\n let entries;\n try {\n entries = await client.fetchWeights();\n } catch {\n // Network/transient error: skip this cycle and retry on the next\n // interval. Swallowed deliberately so a failed poll never surfaces as\n // an unhandled rejection.\n return;\n }\n if (cancelled) return;\n for (const entry of entries) {\n const weights: ComponentWeights = {\n componentId: entry.componentId,\n updatedAt: entry.updatedAt,\n variants: entry.variants.map((v) => ({\n variantId: v.variantId,\n pulls: v.pulls,\n avgReward: v.avgReward ?? 0,\n })),\n };\n updateWeightsStore(entry.componentId, weights);\n }\n };\n void poll();\n const timerId = setInterval(() => void poll(), 60_000);\n return () => {\n cancelled = true;\n clearInterval(timerId);\n };\n }, [client]);\n\n const ssrFallback = props.ssrFallback ?? 'first';\n\n // The client exposed to consumers — wrapped to suppress events while previewing.\n const exposedClient = useMemo(\n () => (client && previewOn ? createPreviewClient(client) : client),\n [client, previewOn],\n );\n\n // Memoized so unrelated parent re-renders don't cascade through every\n // useSentient / useAssignment consumer via a fresh context object.\n const value = useMemo<AdaptiveContextValue>(\n () => ({\n client: exposedClient,\n apiKey: props.apiKey,\n initialAssignments: props.initialAssignments ?? {},\n sessionSegment,\n ssrFallback,\n onAssignment: props.onAssignment,\n initialLayoutOrder: props.initialLayoutOrder ?? null,\n }),\n [\n exposedClient,\n props.apiKey,\n props.initialAssignments,\n sessionSegment,\n ssrFallback,\n props.onAssignment,\n props.initialLayoutOrder,\n ],\n );\n\n return (\n <AdaptiveContext.Provider value={value}>\n {props.children}\n </AdaptiveContext.Provider>\n );\n}\n\n/**\n * Returns the SentientClient, or null until the provider has finished\n * initialising on the client.\n */\nexport function useSentient(): SentientClient | null {\n return useContext(AdaptiveContext).client;\n}\n\n/** Internal: publishable API key carried alongside the client. */\nexport function useAdaptiveApiKey(): string {\n return useContext(AdaptiveContext).apiKey;\n}\n\n/**\n * @deprecated Renamed to `useAdaptiveApiKey`. Kept as an alias so external\n * imports from earlier versions of this package keep working. Will be removed\n * in 1.0.0.\n */\nlet warnedProjectIdAlias = false;\nexport function useAdaptiveProjectId(): string {\n if (\n !warnedProjectIdAlias &&\n typeof process !== 'undefined' &&\n process.env?.NODE_ENV !== 'production'\n ) {\n warnedProjectIdAlias = true;\n console.warn(\n '[@sentientui/react] useAdaptiveProjectId is deprecated; use useAdaptiveApiKey. Will be removed in 1.0.0.',\n );\n }\n return useAdaptiveApiKey();\n}\n\n/** Internal: SSR-preloaded assignments for hydration-safe first render. */\nexport function useInitialAssignments(): Record<string, string> {\n return useContext(AdaptiveContext).initialAssignments;\n}\n\n/** Internal: bandit segment aligned with SSR session upsert. */\nexport function useSessionSegment(): string {\n return useContext(AdaptiveContext).sessionSegment;\n}\n\n/** Internal: SSR fallback strategy when a slot has no preloaded assignment. */\nexport function useSsrFallback(): SsrFallback {\n return useContext(AdaptiveContext).ssrFallback;\n}\n\n/** Internal: forwarding hook for consumer analytics integration. */\nexport function useOnAssignment(): ((componentId: string, variantId: string) => void) | undefined {\n return useContext(AdaptiveContext).onAssignment;\n}\n\n/**\n * Returns the persona-specific section order from SSR, or null when no\n * sections were declared on AdaptiveRoot or reliability is below threshold.\n */\nexport function useLayoutOrder(): string[] | null {\n const contextOrder = useContext(AdaptiveContext).initialLayoutOrder;\n // Test override: `@sentientui/react/testing` sets this to pin layout order\n // synchronously without SSR. Only read when present; inert otherwise.\n if (typeof window !== 'undefined') {\n const override = (window as unknown as { __sentient_layout_override?: string[] }).__sentient_layout_override;\n if (override) return override;\n }\n return contextOrder;\n}\n","/** Per-component weights store with isolated subscriptions. */\n\nexport type VariantWeight = {\n variantId: string;\n pulls: number;\n avgReward: number;\n};\n\nexport type ComponentWeights = {\n componentId: string;\n variants: VariantWeight[];\n updatedAt: number;\n};\n\ntype Listener = (weights: ComponentWeights) => void;\n\nconst store = new Map<string, ComponentWeights>();\nconst listeners = new Map<string, Set<Listener>>();\n\n/**\n * Subscribes a listener to a single component. Returns an unsubscribe function.\n * Updates to other components never trigger this listener.\n */\nexport function subscribe(componentId: string, cb: Listener): () => void {\n let set = listeners.get(componentId);\n if (!set) {\n set = new Set();\n listeners.set(componentId, set);\n }\n set.add(cb);\n return () => {\n set!.delete(cb);\n if (set!.size === 0) listeners.delete(componentId);\n };\n}\n\n/**\n * Replaces the weights for a component and notifies only that component's\n * subscribers.\n */\nexport function update(componentId: string, weights: ComponentWeights): void {\n store.set(componentId, weights);\n const set = listeners.get(componentId);\n if (!set) return;\n for (const cb of set) {\n try {\n cb(weights);\n } catch {\n /* never throw to other listeners */\n }\n }\n}\n\n/**\n * Returns the latest known weights for a component, or null if none seen.\n */\nexport function getWeights(componentId: string): ComponentWeights | null {\n return store.get(componentId) ?? null;\n}\n\n/** Test-only: wipe the entire store. */\nexport function _resetWeightsStore(): void {\n store.clear();\n listeners.clear();\n}\n","import type { SentientClient } from '@sentientui/core';\n\nlet previewOn = false;\nconst listeners = new Set<() => void>();\n\nexport function setPreviewMode(on: boolean): void {\n if (previewOn === on) return;\n previewOn = on;\n for (const fn of listeners) fn();\n}\n\nexport function getPreviewMode(): boolean {\n return previewOn;\n}\n\nexport function subscribePreview(fn: () => void): () => void {\n listeners.add(fn);\n return () => {\n listeners.delete(fn);\n };\n}\n\n/**\n * Wraps a client so it writes nothing: reads pass through, all emitters no-op.\n * Used while previewing variants/personas so no `variant_assigned`, goal, or\n * session events are sent.\n */\nexport function createPreviewClient(inner: SentientClient): SentientClient {\n return {\n track: () => undefined,\n goal: () => undefined,\n componentGoal: () => undefined,\n identify: () => undefined,\n fetchWeights: () => Promise.resolve([]),\n getAssignment: (componentId, segment) => inner.getAssignment(componentId, segment),\n assign: (componentId, variantIds, agentData, agentDataByVariant) =>\n inner.assign(componentId, variantIds, agentData, agentDataByVariant),\n getGraph: () => inner.getGraph(),\n destroy: () => inner.destroy(),\n };\n}\n","export type ScenarioWeight = { variantId: string; pulls: number; avgReward: number };\nexport type ScenarioApiOverride =\n | 'error'\n | number\n | { status?: number; body?: unknown; delayMs?: number };\n\nexport type SentientScenario = {\n variants?: Record<string, string>;\n layout?: string[];\n persona?: string;\n weights?: Record<string, ScenarioWeight[]>;\n api?: Record<string, ScenarioApiOverride>;\n};\n\ntype ScenarioWindow = {\n __sentient_overrides?: Record<string, string>;\n __sentient_layout_override?: string[];\n};\n\n/** Apply a scenario by setting the client-forcing globals the SDK reads. */\nexport function applyScenario(scenario: SentientScenario = {}): void {\n const w = window as unknown as ScenarioWindow;\n w.__sentient_overrides = { ...(scenario.variants ?? {}) };\n if (scenario.layout) w.__sentient_layout_override = scenario.layout;\n else delete w.__sentient_layout_override;\n}\n\n/** Clear all forced state. */\nexport function resetScenario(): void {\n const w = window as unknown as ScenarioWindow;\n delete w.__sentient_overrides;\n delete w.__sentient_layout_override;\n}\n","import { http, HttpResponse } from 'msw';\nimport type { RequestHandler } from 'msw';\nimport { resolveScenario } from './resolve.js';\nimport type { SentientScenario } from './scenario.js';\n\n/** Turn a scenario into MSW handlers stubbing every SDK endpoint + capturing events. */\nexport function scenarioToHandlers(scenario: SentientScenario = {}): RequestHandler[] {\n return [\n http.all('*/v1/*', async ({ request }) => {\n const bodyText =\n request.method === 'GET' || request.method === 'HEAD' ? null : await request.text();\n const r = await resolveScenario(scenario, request.method, request.url, bodyText);\n if (!r) return undefined; // not a stubbed route — let MSW handle passthrough\n if (r.json === undefined) return new HttpResponse(null, { status: r.status });\n return HttpResponse.json(r.json as object, { status: r.status });\n }),\n ];\n}\n","export type CapturedEvent = {\n eventType: string;\n goalType?: string;\n componentId?: string;\n variantId?: string;\n [k: string]: unknown;\n};\n\nconst captured: CapturedEvent[] = [];\n\nexport function recordEvent(e: CapturedEvent): void {\n captured.push(e);\n}\n\nexport function getSentientEvents(): CapturedEvent[] {\n return [...captured];\n}\n\nexport function clearSentientEvents(): void {\n captured.length = 0;\n}\n\n/** True if any captured event is a goal (component goal_achieved or named goal) with this name. */\nexport function hasFiredGoal(events: CapturedEvent[], goalName: string): boolean {\n return events.some(\n (e) => (e.eventType === 'goal_achieved' || e.eventType === 'goal') && e.goalType === goalName,\n );\n}\n","import { recordEvent, type CapturedEvent } from './events.js';\nimport type { SentientScenario, ScenarioApiOverride } from './scenario.js';\n\n/** A framework-agnostic resolved response. `json` undefined ⇒ empty body. */\nexport type ResolvedResponse = { status: number; json?: unknown };\n\nfunction pathOf(url: string): string {\n try { return new URL(url).pathname; } catch { return url.split('?')[0] ?? url; }\n}\n\nasync function apiOverride(scenario: SentientScenario, route: string): Promise<ResolvedResponse | null> {\n const o: ScenarioApiOverride | undefined = scenario.api?.[route];\n if (o === undefined) return null;\n if (o === 'error') return { status: 500 };\n if (typeof o === 'number') return { status: o };\n if (o.delayMs) await new Promise((r) => setTimeout(r, o.delayMs));\n return { status: o.status ?? 200, json: o.body ?? {} };\n}\n\n/**\n * Resolve a request against a scenario. Returns a response, or null for routes\n * outside `/v1/*` (let the caller pass through). Shared by the MSW handlers and\n * the Playwright/Cypress adapters so behaviour can't drift.\n */\nexport async function resolveScenario(\n scenario: SentientScenario,\n _method: string,\n url: string,\n bodyText: string | null,\n): Promise<ResolvedResponse | null> {\n const path = pathOf(url);\n if (!path.includes('/v1/')) return null;\n\n const route = '/v1/' + (path.split('/v1/')[1] ?? '');\n const override = await apiOverride(scenario, route);\n if (override) return override;\n\n const body = bodyText ? (JSON.parse(bodyText) as unknown) : {};\n\n if (route === '/v1/sessions') return { status: 204 };\n\n if (route === '/v1/events') {\n for (const e of body as CapturedEvent[]) recordEvent(e);\n return { status: 204 };\n }\n\n if (route === '/v1/goals') {\n recordEvent({ eventType: 'goal', goalType: (body as { name?: string }).name });\n return { status: 204 };\n }\n\n if (route === '/v1/assign') {\n const b = body as { componentId: string; variantIds?: string[] };\n const variantId = scenario.variants?.[b.componentId] ?? b.variantIds?.[0] ?? 'control';\n return { status: 200, json: { variantId, assignmentTtlMs: 60_000 } };\n }\n\n if (route === '/v1/decide') {\n const b = body as { sections?: { id: string }[] };\n const layoutOrder = scenario.layout ?? (b.sections ?? []).map((s) => s.id);\n return { status: 200, json: { layoutOrder, assignments: scenario.variants ?? {}, persona: scenario.persona ?? 'unknown', confidence: 1 } };\n }\n\n if (route === '/v1/explain') {\n const b = body as { sections?: { id: string }[]; persona?: string };\n const layoutOrder = scenario.layout ?? (b.sections ?? []).map((s) => s.id);\n const persona = b.persona ?? scenario.persona ?? 'unknown';\n return { status: 200, json: { layoutOrder, assignments: scenario.variants ?? {}, persona, reasons: [] } };\n }\n\n if (route === '/v1/weights') {\n const components = Object.entries(scenario.weights ?? {}).map(([componentId, variants]) => ({ componentId, updatedAt: 0, variants }));\n return { status: 200, json: { components } };\n }\n\n return null;\n}\n","import { resolveScenario } from './resolve.js';\nimport { getSentientEvents, type CapturedEvent } from './events.js';\nimport type { SentientScenario } from './scenario.js';\n\ntype InitData = { overrides: Record<string, string>; layout: string[] | null };\n\ntype PwRoute = {\n request(): { method(): string; url(): string; postData(): string | null };\n fulfill(r: { status: number; contentType?: string; body?: string }): Promise<void>;\n continue(): Promise<void>;\n};\n\n/** Structural subset of Playwright's `Page` — avoids a hard dependency on @playwright/test. */\ntype PwPage = {\n addInitScript(script: (arg: InitData) => void, arg: InitData): Promise<void>;\n route(url: string, handler: (route: PwRoute) => unknown): Promise<void>;\n};\n\n/**\n * Make a Playwright `page` serve a SentientUI scenario: forces variants/layout\n * before load and stubs every `/v1/*` request from the scenario, capturing\n * posted events. Returns a handle exposing `.events()`.\n */\nexport async function mockSentient(\n page: PwPage,\n scenario: SentientScenario = {},\n): Promise<{ events: () => CapturedEvent[] }> {\n const initData: InitData = { overrides: scenario.variants ?? {}, layout: scenario.layout ?? null };\n await page.addInitScript((data: InitData) => {\n (window as unknown as { __sentient_overrides?: unknown }).__sentient_overrides = data.overrides;\n if (data.layout) {\n (window as unknown as { __sentient_layout_override?: unknown }).__sentient_layout_override = data.layout;\n }\n }, initData);\n\n await page.route('**/v1/**', async (route) => {\n const req = route.request();\n const resolved = await resolveScenario(scenario, req.method(), req.url(), req.postData());\n if (!resolved) return route.continue();\n await route.fulfill({\n status: resolved.status,\n contentType: 'application/json',\n body: JSON.stringify(resolved.json ?? {}),\n });\n });\n\n return { events: () => getSentientEvents() };\n}\n","import { resolveScenario } from './resolve.js';\nimport type { SentientScenario } from './scenario.js';\n\ntype CyReq = {\n method: string;\n url: string;\n body: unknown;\n reply(r: { statusCode: number; body?: unknown }): void;\n};\n\n/** Structural subset of Cypress's `cy` — avoids a hard dependency on cypress. */\ntype Cy = {\n intercept(url: string, handler: (req: CyReq) => void | Promise<void>): unknown;\n on(event: string, cb: (win: Record<string, unknown>) => void): unknown;\n};\n\n/**\n * Make Cypress serve a SentientUI scenario: forces variants/layout on the app\n * window before load and stubs every `/v1/*` request from the scenario. Call in\n * a `beforeEach` before `cy.visit`.\n *\n * Note: for event assertions in Cypress, alias the intercept (`cy.intercept(...).as('ev')`)\n * and `cy.wait('@ev')` — captured module state does not cross the browser/Node boundary.\n */\nexport function mockSentientCypress(cy: Cy, scenario: SentientScenario = {}): void {\n const overrides = scenario.variants ?? {};\n const layout = scenario.layout ?? null;\n\n cy.on('window:before:load', (win) => {\n win.__sentient_overrides = overrides;\n if (layout) win.__sentient_layout_override = layout;\n });\n\n cy.intercept('**/v1/**', async (req) => {\n const resolved = await resolveScenario(\n scenario,\n req.method,\n req.url,\n req.body != null ? JSON.stringify(req.body) : null,\n );\n if (!resolved) return; // passthrough\n req.reply({ statusCode: resolved.status, body: (resolved.json ?? '') as unknown });\n });\n}\n"],"mappings":"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","names":["testing_exports","__export","applyScenario","clearSentientEvents","getSentientEvents","hasFiredGoal","mockSentient","mockSentientCypress","renderWithSentient","resetScenario","resolveScenario","scenarioToHandlers","setupSentientTests","__toCommonJS","import_react","import_react","import_core","store","listeners","update","componentId","weights","store","set","listeners","cb","e","previewOn","listeners","getPreviewMode","previewOn","subscribePreview","fn","listeners","createPreviewClient","inner","componentId","segment","variantIds","agentData","agentDataByVariant","import_jsx_runtime","deriveDefaultSegment","_a","_b","device","source","e","AdaptiveContext","AdaptiveProvider","props","client","setClient","sessionSegment","previewOn","setPreviewOn","getPreviewMode","subscribePreview","prev","config","cancelled","created","initGraph","__spreadProps","__spreadValues","poll","entries","entry","weights","v","update","timerId","ssrFallback","exposedClient","createPreviewClient","value","applyScenario","scenario","_a","w","__spreadValues","resetScenario","import_msw","captured","recordEvent","getSentientEvents","clearSentientEvents","hasFiredGoal","events","goalName","e","pathOf","url","_a","e","apiOverride","scenario","route","_b","_c","o","r","resolveScenario","_method","bodyText","_d","_e","_f","_g","_h","_i","_j","_k","_l","_m","_n","_o","path","override","body","recordEvent","b","s","layoutOrder","persona","componentId","variants","scenarioToHandlers","scenario","request","bodyText","r","resolveScenario","mockSentient","page","scenario","_a","_b","initData","data","route","req","resolved","resolveScenario","getSentientEvents","mockSentientCypress","cy","scenario","_a","_b","overrides","layout","win","req","resolved","resolveScenario","import_jsx_runtime","renderWithSentient","ui","scenario","options","applyScenario","AdaptiveProvider","setupSentientTests","_a","g","resetScenario"]}
@@ -0,0 +1,3 @@
1
+ 'use client';
2
+ var z=Object.defineProperty,U=Object.defineProperties;var X=Object.getOwnPropertyDescriptors;var T=Object.getOwnPropertySymbols;var q=Object.prototype.hasOwnProperty,Q=Object.prototype.propertyIsEnumerable;var D=(e,t,n)=>t in e?z(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,b=(e,t)=>{for(var n in t||(t={}))q.call(t,n)&&D(e,n,t[n]);if(T)for(var n of T(t))Q.call(t,n)&&D(e,n,t[n]);return e},F=(e,t)=>U(e,X(t));import{render as me}from"@testing-library/react";import{createContext as te,useContext as be,useEffect as C,useMemo as G,useState as A}from"react";import{detectDeviceClass as ne,detectTrafficSource as ie,init as re}from"@sentientui/core";var Y=new Map,Z=new Map;function K(e,t){Y.set(e,t);let n=Z.get(e);if(n)for(let i of n)try{i(t)}catch(s){}}var ee=!1,L=new Set;function x(){return ee}function N(e){return L.add(e),()=>{L.delete(e)}}function M(e){return{track:()=>{},goal:()=>{},componentGoal:()=>{},identify:()=>{},fetchWeights:()=>Promise.resolve([]),getAssignment:(t,n)=>e.getAssignment(t,n),assign:(t,n,i,s)=>e.assign(t,n,i,s),getGraph:()=>e.getGraph(),destroy:()=>e.destroy()}}import{jsx as ae}from"react/jsx-runtime";function oe(){var e,t;if(typeof window=="undefined")return"desktop:direct";try{let n=ne((e=navigator.userAgent)!=null?e:""),i=ie((t=document.referrer)!=null?t:"",window.location.origin);return`${n}:${i}`}catch(n){return"desktop:direct"}}var se=te({client:null,apiKey:"",initialAssignments:{},sessionSegment:"desktop:direct",ssrFallback:"first",onAssignment:void 0,initialLayoutOrder:null});function V(e){var y;let[t,n]=A(null),[i]=A(()=>{var u;return(u=e.sessionSegment)!=null?u:oe()}),[s,r]=A(x());C(()=>N(()=>r(x())),[]),C(()=>{if(e.consent===!1&&!e.preConsentBehavior){n(c=>(c==null||c.destroy(),null));return}let u={apiKey:e.apiKey,context:e.context,debug:e.debug,initialAssignments:e.initialAssignments,sessionSegment:i,consent:e.consent,preConsentBehavior:e.preConsentBehavior,respectDoNotTrack:e.respectDoNotTrack,ssrSessionId:e.ssrSessionId,country:e.country},l=!1,d=null;return e.enableGraph?import("@sentientui/core/graph").then(({init:c})=>{l||(d=c(F(b({},u),{graph:!0})),n(d))}):(d=re(u),n(d)),()=>{l=!0,d==null||d.destroy()}},[e.consent]),C(()=>{if(!t)return;let u=!1,l=async()=>{if(u)return;let c;try{c=await t.fetchWeights()}catch(g){return}if(!u)for(let g of c){let h={componentId:g.componentId,updatedAt:g.updatedAt,variants:g.variants.map(m=>{var S;return{variantId:m.variantId,pulls:m.pulls,avgReward:(S=m.avgReward)!=null?S:0}})};K(g.componentId,h)}};l();let d=setInterval(()=>{l()},6e4);return()=>{u=!0,clearInterval(d)}},[t]);let o=(y=e.ssrFallback)!=null?y:"first",a=G(()=>t&&s?M(t):t,[t,s]),p=G(()=>{var u,l;return{client:a,apiKey:e.apiKey,initialAssignments:(u=e.initialAssignments)!=null?u:{},sessionSegment:i,ssrFallback:o,onAssignment:e.onAssignment,initialLayoutOrder:(l=e.initialLayoutOrder)!=null?l:null}},[a,e.apiKey,e.initialAssignments,i,o,e.onAssignment,e.initialLayoutOrder]);return ae(se.Provider,{value:p,children:e.children})}function H(e={}){var n;let t=window;t.__sentient_overrides=b({},(n=e.variants)!=null?n:{}),e.layout?t.__sentient_layout_override=e.layout:delete t.__sentient_layout_override}function B(){let e=window;delete e.__sentient_overrides,delete e.__sentient_layout_override}import{http as pe,HttpResponse as J}from"msw";var k=[];function R(e){k.push(e)}function P(){return[...k]}function ue(){k.length=0}function de(e,t){return e.some(n=>(n.eventType==="goal_achieved"||n.eventType==="goal")&&n.goalType===t)}function le(e){var t;try{return new URL(e).pathname}catch(n){return(t=e.split("?")[0])!=null?t:e}}async function ce(e,t){var i,s,r;let n=(i=e.api)==null?void 0:i[t];return n===void 0?null:n==="error"?{status:500}:typeof n=="number"?{status:n}:(n.delayMs&&await new Promise(o=>setTimeout(o,n.delayMs)),{status:(s=n.status)!=null?s:200,json:(r=n.body)!=null?r:{}})}async function f(e,t,n,i){var p,y,u,l,d,c,g,h,m,S,I,O,E,W,j;let s=le(n);if(!s.includes("/v1/"))return null;let r="/v1/"+((p=s.split("/v1/")[1])!=null?p:""),o=await ce(e,r);if(o)return o;let a=i?JSON.parse(i):{};if(r==="/v1/sessions")return{status:204};if(r==="/v1/events"){for(let v of a)R(v);return{status:204}}if(r==="/v1/goals")return R({eventType:"goal",goalType:a.name}),{status:204};if(r==="/v1/assign"){let v=a;return{status:200,json:{variantId:(d=(l=(y=e.variants)==null?void 0:y[v.componentId])!=null?l:(u=v.variantIds)==null?void 0:u[0])!=null?d:"control",assignmentTtlMs:6e4}}}if(r==="/v1/decide"){let v=a;return{status:200,json:{layoutOrder:(g=e.layout)!=null?g:((c=v.sections)!=null?c:[]).map(_=>_.id),assignments:(h=e.variants)!=null?h:{},persona:(m=e.persona)!=null?m:"unknown",confidence:1}}}if(r==="/v1/explain"){let v=a,w=(I=e.layout)!=null?I:((S=v.sections)!=null?S:[]).map($=>$.id),_=(E=(O=v.persona)!=null?O:e.persona)!=null?E:"unknown";return{status:200,json:{layoutOrder:w,assignments:(W=e.variants)!=null?W:{},persona:_,reasons:[]}}}return r==="/v1/weights"?{status:200,json:{components:Object.entries((j=e.weights)!=null?j:{}).map(([w,_])=>({componentId:w,updatedAt:0,variants:_}))}}:null}function ve(e={}){return[pe.all("*/v1/*",async({request:t})=>{let n=t.method==="GET"||t.method==="HEAD"?null:await t.text(),i=await f(e,t.method,t.url,n);if(i)return i.json===void 0?new J(null,{status:i.status}):J.json(i.json,{status:i.status})})]}async function ge(e,t={}){var i,s;let n={overrides:(i=t.variants)!=null?i:{},layout:(s=t.layout)!=null?s:null};return await e.addInitScript(r=>{window.__sentient_overrides=r.overrides,r.layout&&(window.__sentient_layout_override=r.layout)},n),await e.route("**/v1/**",async r=>{var p;let o=r.request(),a=await f(t,o.method(),o.url(),o.postData());if(!a)return r.continue();await r.fulfill({status:a.status,contentType:"application/json",body:JSON.stringify((p=a.json)!=null?p:{})})}),{events:()=>P()}}function fe(e,t={}){var s,r;let n=(s=t.variants)!=null?s:{},i=(r=t.layout)!=null?r:null;e.on("window:before:load",o=>{o.__sentient_overrides=n,i&&(o.__sentient_layout_override=i)}),e.intercept("**/v1/**",async o=>{var p;let a=await f(t,o.method,o.url,o.body!=null?JSON.stringify(o.body):null);a&&o.reply({statusCode:a.status,body:(p=a.json)!=null?p:""})})}import{jsx as ye}from"react/jsx-runtime";function Je(e,t={},n){return H(t),me(ye(V,{apiKey:"pk_test",context:"saas",consent:!1,children:e}),n)}function $e(){var t;let e=globalThis;(t=e.afterEach)==null||t.call(e,()=>B())}export{H as applyScenario,ue as clearSentientEvents,P as getSentientEvents,de as hasFiredGoal,ge as mockSentient,fe as mockSentientCypress,Je as renderWithSentient,B as resetScenario,f as resolveScenario,ve as scenarioToHandlers,$e as setupSentientTests};
3
+ //# sourceMappingURL=testing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/testing/index.tsx","../src/provider.tsx","../src/weights-store.ts","../src/preview-mode.ts","../src/testing/scenario.ts","../src/testing/handlers.ts","../src/testing/events.ts","../src/testing/resolve.ts","../src/testing/playwright.ts","../src/testing/cypress.ts"],"sourcesContent":["import type { ReactElement } from 'react';\nimport { render, type RenderOptions, type RenderResult } from '@testing-library/react';\nimport { AdaptiveProvider } from '../provider.js';\nimport { applyScenario, resetScenario, type SentientScenario } from './scenario.js';\n\nexport type { SentientScenario };\nexport { applyScenario, resetScenario };\nexport { scenarioToHandlers } from './handlers.js';\nexport { resolveScenario, type ResolvedResponse } from './resolve.js';\nexport { getSentientEvents, clearSentientEvents, hasFiredGoal, type CapturedEvent } from './events.js';\nexport type { ScenarioWeight, ScenarioApiOverride } from './scenario.js';\nexport { mockSentient } from './playwright.js';\nexport { mockSentientCypress } from './cypress.js';\n\n/**\n * Render `ui` under a SentientUI provider configured for tests: consent is off,\n * so the SDK never initialises a client and every <Adaptive> renders its control\n * variant with zero network. A scenario forces specific variants/layout.\n */\nexport function renderWithSentient(\n ui: ReactElement,\n scenario: SentientScenario = {},\n options?: RenderOptions,\n): RenderResult {\n applyScenario(scenario);\n return render(\n <AdaptiveProvider apiKey=\"pk_test\" context=\"saas\" consent={false}>\n {ui}\n </AdaptiveProvider>,\n options,\n );\n}\n\n/** Call in a test setup file to reset forced state after each test. */\nexport function setupSentientTests(): void {\n const g = globalThis as unknown as { afterEach?: (fn: () => void) => void };\n g.afterEach?.(() => resetScenario());\n}\n","'use client';\n\ndeclare const process: { env?: { NODE_ENV?: string } } | undefined;\n\nimport { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';\nimport {\n detectDeviceClass,\n detectTrafficSource,\n init,\n type SentientClient,\n type SentientConfig,\n} from '@sentientui/core';\nimport { update as updateWeightsStore, type ComponentWeights } from './weights-store.js';\nimport { getPreviewMode, subscribePreview, createPreviewClient } from './preview-mode.js';\n\n/**\n * Mirrors the segment derivation inside core `init()` so the cache key used\n * by `useAssignment` always matches the key `assign()` writes under. Before\n * this, the context defaulted to 'desktop:direct' while core used the\n * detected segment — a systematic cache miss for every integration that\n * didn't pass `sessionSegment` explicitly.\n */\nfunction deriveDefaultSegment(): string {\n if (typeof window === 'undefined') return 'desktop:direct';\n try {\n const device = detectDeviceClass(navigator.userAgent ?? '');\n const source = detectTrafficSource(document.referrer ?? '', window.location.origin);\n return `${device}:${source}`;\n } catch {\n return 'desktop:direct';\n }\n}\n\n/** How to render adaptive slots during SSR when assignments are not preloaded. */\nexport type SsrFallback = 'first' | 'none';\n\ntype AdaptiveContextValue = {\n client: SentientClient | null;\n // The publishable API key (pk_…). Historically named `projectId` because the\n // API uses it as the project identifier on the wire, but it is the API key,\n // not the project UUID. Field renamed for clarity.\n apiKey: string;\n initialAssignments: Record<string, string>;\n sessionSegment: string;\n ssrFallback: SsrFallback;\n onAssignment: ((componentId: string, variantId: string) => void) | undefined;\n initialLayoutOrder: string[] | null;\n};\n\nconst AdaptiveContext = createContext<AdaptiveContextValue>({\n client: null,\n apiKey: '',\n initialAssignments: {},\n sessionSegment: 'desktop:direct',\n ssrFallback: 'first',\n onAssignment: undefined,\n initialLayoutOrder: null,\n});\n\nexport type AdaptiveProviderProps = {\n apiKey: string;\n context: SentientConfig['context'];\n debug?: boolean;\n /**\n * SSR-preloaded assignments from `preloadAssignments()` / `loadAdaptiveAssignments()`.\n * Passed through to `useAssignment` as synchronous initial state so crawlers and\n * the first paint see real content (recommended for SEO).\n */\n initialAssignments?: Record<string, string>;\n /**\n * Bandit segment from SSR (`device:source`). Keeps cache, assign, and worker\n * weights on one row — must match `loadAdaptiveAssignments` / session upsert.\n */\n sessionSegment?: string;\n /**\n * When no `initialAssignments` exist for a component, `'first'` renders\n * `variantIds[0]` in server HTML (safe default for SEO). Use `'none'` only for\n * decorative slots marked `clientOnly`.\n * @default 'first'\n */\n ssrFallback?: SsrFallback;\n /**\n * Consent gate. When `false` the SDK is not initialised and no events are\n * sent. Flip to `true` (e.g. after the user accepts the cookie banner) to\n * initialise and begin tracking.\n */\n consent?: boolean;\n /**\n * Behavior before consent is granted. Pass `'statistical_winner'` to serve the\n * best-performing variant via `GET /v1/winner` with zero tracking while the\n * consent banner is showing. Requires `consent: false`.\n * @see SentientConfig.preConsentBehavior\n */\n preConsentBehavior?: 'statistical_winner' | 'control';\n /**\n * Honor the browser's Do Not Track signal. Defaults to `true`: when DNT is\n * enabled the SDK sets no cookies and sends no tracking data (overriding\n * `consent: true`). Set `false` to make your own consent gate authoritative.\n * @see SentientConfig.respectDoNotTrack\n */\n respectDoNotTrack?: boolean;\n /**\n * Called once per component the first time a variant is resolved for that\n * component in this session. Use to forward assignments to your own analytics\n * (Mixpanel, PostHog, Segment, etc.) without having to wrap `useAssignment`.\n */\n onAssignment?: (componentId: string, variantId: string) => void;\n /**\n * SSR-preloaded section order from `loadAdaptiveDecision()`.\n * Pass the `layoutOrder` field from `DecideResult`. When set,\n * `useLayoutOrder()` returns this on first render so there is no layout shift.\n */\n initialLayoutOrder?: string[] | null;\n /**\n * Session ID generated during SSR (the `sessionId` field returned by\n * `loadAdaptiveAssignments` / `loadAdaptiveDecision`). When provided and no\n * existing session cookie or localStorage entry is found, the client adopts\n * this ID so events and goals are attributed to the same session the server\n * used for variant assignment.\n */\n ssrSessionId?: string;\n /**\n * ISO 3166-1 alpha-2 country code. Pass the value of the `CF-IPCountry`\n * header from your Next.js server component to populate country on landing\n * sessions without client-side geo lookup.\n */\n country?: string;\n /**\n * Enable DOM graph scanning + page-structure sync. When `true`, the provider\n * dynamically loads `@sentientui/core/graph` and uses its graph-capable\n * `init()` for the single client, so the SDK scans your page structure and\n * syncs it to power the dashboard graph page. Left off (default), the lean\n * bundle is used and the graph entry is never loaded.\n */\n enableGraph?: boolean;\n children: ReactNode;\n};\n\n/**\n * Initialises the Sentient core SDK in a useEffect (SSR-safe) and exposes the\n * client via React context. Re-initialises when `consent` changes.\n */\nexport function AdaptiveProvider(props: AdaptiveProviderProps): JSX.Element {\n const [client, setClient] = useState<SentientClient | null>(null);\n // Derived once per mount: identical to what core init() computes, so cache\n // reads (context segment) and cache writes (core segment) always agree.\n const [sessionSegment] = useState(() => props.sessionSegment ?? deriveDefaultSegment());\n // Devtools preview: when on, expose an event-suppressing client so previewing\n // variants/personas writes nothing. Off by default (inert in production).\n const [previewOn, setPreviewOn] = useState(getPreviewMode());\n useEffect(() => subscribePreview(() => setPreviewOn(getPreviewMode())), []);\n\n useEffect(() => {\n // When consent is explicitly false with no preConsentBehavior, tear down any existing client.\n if (props.consent === false && !props.preConsentBehavior) {\n setClient((prev: SentientClient | null) => {\n prev?.destroy();\n return null;\n });\n return;\n }\n\n const config = {\n apiKey: props.apiKey,\n context: props.context,\n debug: props.debug,\n initialAssignments: props.initialAssignments,\n sessionSegment,\n consent: props.consent,\n preConsentBehavior: props.preConsentBehavior,\n respectDoNotTrack: props.respectDoNotTrack,\n ssrSessionId: props.ssrSessionId,\n country: props.country,\n };\n\n // Track the client created by this effect run so cleanup destroys exactly\n // the right one, and so a late-resolving dynamic import can bail if the\n // effect was already torn down (unmount / consent change).\n let cancelled = false;\n let created: SentientClient | null = null;\n\n if (props.enableGraph) {\n // Load the graph entry only when asked — keeps the DOM scanner out of the\n // lean bundle. The provider still creates ONE client (graph-capable).\n void import('@sentientui/core/graph').then(({ init: initGraph }) => {\n if (cancelled) return;\n created = initGraph({ ...config, graph: true });\n setClient(created);\n });\n } else {\n created = init(config);\n setClient(created);\n }\n\n return () => {\n cancelled = true;\n created?.destroy();\n };\n // Re-init when consent changes. Other props (incl. enableGraph) are\n // intentionally stable for a session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [props.consent]);\n\n // Poll /v1/weights every 60 s so long-lived sessions see updated bandit weights\n // without a page reload. useAssignment subscribers react via weights-store.\n useEffect(() => {\n if (!client) return;\n let cancelled = false;\n const poll = async (): Promise<void> => {\n if (cancelled) return;\n let entries;\n try {\n entries = await client.fetchWeights();\n } catch {\n // Network/transient error: skip this cycle and retry on the next\n // interval. Swallowed deliberately so a failed poll never surfaces as\n // an unhandled rejection.\n return;\n }\n if (cancelled) return;\n for (const entry of entries) {\n const weights: ComponentWeights = {\n componentId: entry.componentId,\n updatedAt: entry.updatedAt,\n variants: entry.variants.map((v) => ({\n variantId: v.variantId,\n pulls: v.pulls,\n avgReward: v.avgReward ?? 0,\n })),\n };\n updateWeightsStore(entry.componentId, weights);\n }\n };\n void poll();\n const timerId = setInterval(() => void poll(), 60_000);\n return () => {\n cancelled = true;\n clearInterval(timerId);\n };\n }, [client]);\n\n const ssrFallback = props.ssrFallback ?? 'first';\n\n // The client exposed to consumers — wrapped to suppress events while previewing.\n const exposedClient = useMemo(\n () => (client && previewOn ? createPreviewClient(client) : client),\n [client, previewOn],\n );\n\n // Memoized so unrelated parent re-renders don't cascade through every\n // useSentient / useAssignment consumer via a fresh context object.\n const value = useMemo<AdaptiveContextValue>(\n () => ({\n client: exposedClient,\n apiKey: props.apiKey,\n initialAssignments: props.initialAssignments ?? {},\n sessionSegment,\n ssrFallback,\n onAssignment: props.onAssignment,\n initialLayoutOrder: props.initialLayoutOrder ?? null,\n }),\n [\n exposedClient,\n props.apiKey,\n props.initialAssignments,\n sessionSegment,\n ssrFallback,\n props.onAssignment,\n props.initialLayoutOrder,\n ],\n );\n\n return (\n <AdaptiveContext.Provider value={value}>\n {props.children}\n </AdaptiveContext.Provider>\n );\n}\n\n/**\n * Returns the SentientClient, or null until the provider has finished\n * initialising on the client.\n */\nexport function useSentient(): SentientClient | null {\n return useContext(AdaptiveContext).client;\n}\n\n/** Internal: publishable API key carried alongside the client. */\nexport function useAdaptiveApiKey(): string {\n return useContext(AdaptiveContext).apiKey;\n}\n\n/**\n * @deprecated Renamed to `useAdaptiveApiKey`. Kept as an alias so external\n * imports from earlier versions of this package keep working. Will be removed\n * in 1.0.0.\n */\nlet warnedProjectIdAlias = false;\nexport function useAdaptiveProjectId(): string {\n if (\n !warnedProjectIdAlias &&\n typeof process !== 'undefined' &&\n process.env?.NODE_ENV !== 'production'\n ) {\n warnedProjectIdAlias = true;\n console.warn(\n '[@sentientui/react] useAdaptiveProjectId is deprecated; use useAdaptiveApiKey. Will be removed in 1.0.0.',\n );\n }\n return useAdaptiveApiKey();\n}\n\n/** Internal: SSR-preloaded assignments for hydration-safe first render. */\nexport function useInitialAssignments(): Record<string, string> {\n return useContext(AdaptiveContext).initialAssignments;\n}\n\n/** Internal: bandit segment aligned with SSR session upsert. */\nexport function useSessionSegment(): string {\n return useContext(AdaptiveContext).sessionSegment;\n}\n\n/** Internal: SSR fallback strategy when a slot has no preloaded assignment. */\nexport function useSsrFallback(): SsrFallback {\n return useContext(AdaptiveContext).ssrFallback;\n}\n\n/** Internal: forwarding hook for consumer analytics integration. */\nexport function useOnAssignment(): ((componentId: string, variantId: string) => void) | undefined {\n return useContext(AdaptiveContext).onAssignment;\n}\n\n/**\n * Returns the persona-specific section order from SSR, or null when no\n * sections were declared on AdaptiveRoot or reliability is below threshold.\n */\nexport function useLayoutOrder(): string[] | null {\n const contextOrder = useContext(AdaptiveContext).initialLayoutOrder;\n // Test override: `@sentientui/react/testing` sets this to pin layout order\n // synchronously without SSR. Only read when present; inert otherwise.\n if (typeof window !== 'undefined') {\n const override = (window as unknown as { __sentient_layout_override?: string[] }).__sentient_layout_override;\n if (override) return override;\n }\n return contextOrder;\n}\n","/** Per-component weights store with isolated subscriptions. */\n\nexport type VariantWeight = {\n variantId: string;\n pulls: number;\n avgReward: number;\n};\n\nexport type ComponentWeights = {\n componentId: string;\n variants: VariantWeight[];\n updatedAt: number;\n};\n\ntype Listener = (weights: ComponentWeights) => void;\n\nconst store = new Map<string, ComponentWeights>();\nconst listeners = new Map<string, Set<Listener>>();\n\n/**\n * Subscribes a listener to a single component. Returns an unsubscribe function.\n * Updates to other components never trigger this listener.\n */\nexport function subscribe(componentId: string, cb: Listener): () => void {\n let set = listeners.get(componentId);\n if (!set) {\n set = new Set();\n listeners.set(componentId, set);\n }\n set.add(cb);\n return () => {\n set!.delete(cb);\n if (set!.size === 0) listeners.delete(componentId);\n };\n}\n\n/**\n * Replaces the weights for a component and notifies only that component's\n * subscribers.\n */\nexport function update(componentId: string, weights: ComponentWeights): void {\n store.set(componentId, weights);\n const set = listeners.get(componentId);\n if (!set) return;\n for (const cb of set) {\n try {\n cb(weights);\n } catch {\n /* never throw to other listeners */\n }\n }\n}\n\n/**\n * Returns the latest known weights for a component, or null if none seen.\n */\nexport function getWeights(componentId: string): ComponentWeights | null {\n return store.get(componentId) ?? null;\n}\n\n/** Test-only: wipe the entire store. */\nexport function _resetWeightsStore(): void {\n store.clear();\n listeners.clear();\n}\n","import type { SentientClient } from '@sentientui/core';\n\nlet previewOn = false;\nconst listeners = new Set<() => void>();\n\nexport function setPreviewMode(on: boolean): void {\n if (previewOn === on) return;\n previewOn = on;\n for (const fn of listeners) fn();\n}\n\nexport function getPreviewMode(): boolean {\n return previewOn;\n}\n\nexport function subscribePreview(fn: () => void): () => void {\n listeners.add(fn);\n return () => {\n listeners.delete(fn);\n };\n}\n\n/**\n * Wraps a client so it writes nothing: reads pass through, all emitters no-op.\n * Used while previewing variants/personas so no `variant_assigned`, goal, or\n * session events are sent.\n */\nexport function createPreviewClient(inner: SentientClient): SentientClient {\n return {\n track: () => undefined,\n goal: () => undefined,\n componentGoal: () => undefined,\n identify: () => undefined,\n fetchWeights: () => Promise.resolve([]),\n getAssignment: (componentId, segment) => inner.getAssignment(componentId, segment),\n assign: (componentId, variantIds, agentData, agentDataByVariant) =>\n inner.assign(componentId, variantIds, agentData, agentDataByVariant),\n getGraph: () => inner.getGraph(),\n destroy: () => inner.destroy(),\n };\n}\n","export type ScenarioWeight = { variantId: string; pulls: number; avgReward: number };\nexport type ScenarioApiOverride =\n | 'error'\n | number\n | { status?: number; body?: unknown; delayMs?: number };\n\nexport type SentientScenario = {\n variants?: Record<string, string>;\n layout?: string[];\n persona?: string;\n weights?: Record<string, ScenarioWeight[]>;\n api?: Record<string, ScenarioApiOverride>;\n};\n\ntype ScenarioWindow = {\n __sentient_overrides?: Record<string, string>;\n __sentient_layout_override?: string[];\n};\n\n/** Apply a scenario by setting the client-forcing globals the SDK reads. */\nexport function applyScenario(scenario: SentientScenario = {}): void {\n const w = window as unknown as ScenarioWindow;\n w.__sentient_overrides = { ...(scenario.variants ?? {}) };\n if (scenario.layout) w.__sentient_layout_override = scenario.layout;\n else delete w.__sentient_layout_override;\n}\n\n/** Clear all forced state. */\nexport function resetScenario(): void {\n const w = window as unknown as ScenarioWindow;\n delete w.__sentient_overrides;\n delete w.__sentient_layout_override;\n}\n","import { http, HttpResponse } from 'msw';\nimport type { RequestHandler } from 'msw';\nimport { resolveScenario } from './resolve.js';\nimport type { SentientScenario } from './scenario.js';\n\n/** Turn a scenario into MSW handlers stubbing every SDK endpoint + capturing events. */\nexport function scenarioToHandlers(scenario: SentientScenario = {}): RequestHandler[] {\n return [\n http.all('*/v1/*', async ({ request }) => {\n const bodyText =\n request.method === 'GET' || request.method === 'HEAD' ? null : await request.text();\n const r = await resolveScenario(scenario, request.method, request.url, bodyText);\n if (!r) return undefined; // not a stubbed route — let MSW handle passthrough\n if (r.json === undefined) return new HttpResponse(null, { status: r.status });\n return HttpResponse.json(r.json as object, { status: r.status });\n }),\n ];\n}\n","export type CapturedEvent = {\n eventType: string;\n goalType?: string;\n componentId?: string;\n variantId?: string;\n [k: string]: unknown;\n};\n\nconst captured: CapturedEvent[] = [];\n\nexport function recordEvent(e: CapturedEvent): void {\n captured.push(e);\n}\n\nexport function getSentientEvents(): CapturedEvent[] {\n return [...captured];\n}\n\nexport function clearSentientEvents(): void {\n captured.length = 0;\n}\n\n/** True if any captured event is a goal (component goal_achieved or named goal) with this name. */\nexport function hasFiredGoal(events: CapturedEvent[], goalName: string): boolean {\n return events.some(\n (e) => (e.eventType === 'goal_achieved' || e.eventType === 'goal') && e.goalType === goalName,\n );\n}\n","import { recordEvent, type CapturedEvent } from './events.js';\nimport type { SentientScenario, ScenarioApiOverride } from './scenario.js';\n\n/** A framework-agnostic resolved response. `json` undefined ⇒ empty body. */\nexport type ResolvedResponse = { status: number; json?: unknown };\n\nfunction pathOf(url: string): string {\n try { return new URL(url).pathname; } catch { return url.split('?')[0] ?? url; }\n}\n\nasync function apiOverride(scenario: SentientScenario, route: string): Promise<ResolvedResponse | null> {\n const o: ScenarioApiOverride | undefined = scenario.api?.[route];\n if (o === undefined) return null;\n if (o === 'error') return { status: 500 };\n if (typeof o === 'number') return { status: o };\n if (o.delayMs) await new Promise((r) => setTimeout(r, o.delayMs));\n return { status: o.status ?? 200, json: o.body ?? {} };\n}\n\n/**\n * Resolve a request against a scenario. Returns a response, or null for routes\n * outside `/v1/*` (let the caller pass through). Shared by the MSW handlers and\n * the Playwright/Cypress adapters so behaviour can't drift.\n */\nexport async function resolveScenario(\n scenario: SentientScenario,\n _method: string,\n url: string,\n bodyText: string | null,\n): Promise<ResolvedResponse | null> {\n const path = pathOf(url);\n if (!path.includes('/v1/')) return null;\n\n const route = '/v1/' + (path.split('/v1/')[1] ?? '');\n const override = await apiOverride(scenario, route);\n if (override) return override;\n\n const body = bodyText ? (JSON.parse(bodyText) as unknown) : {};\n\n if (route === '/v1/sessions') return { status: 204 };\n\n if (route === '/v1/events') {\n for (const e of body as CapturedEvent[]) recordEvent(e);\n return { status: 204 };\n }\n\n if (route === '/v1/goals') {\n recordEvent({ eventType: 'goal', goalType: (body as { name?: string }).name });\n return { status: 204 };\n }\n\n if (route === '/v1/assign') {\n const b = body as { componentId: string; variantIds?: string[] };\n const variantId = scenario.variants?.[b.componentId] ?? b.variantIds?.[0] ?? 'control';\n return { status: 200, json: { variantId, assignmentTtlMs: 60_000 } };\n }\n\n if (route === '/v1/decide') {\n const b = body as { sections?: { id: string }[] };\n const layoutOrder = scenario.layout ?? (b.sections ?? []).map((s) => s.id);\n return { status: 200, json: { layoutOrder, assignments: scenario.variants ?? {}, persona: scenario.persona ?? 'unknown', confidence: 1 } };\n }\n\n if (route === '/v1/explain') {\n const b = body as { sections?: { id: string }[]; persona?: string };\n const layoutOrder = scenario.layout ?? (b.sections ?? []).map((s) => s.id);\n const persona = b.persona ?? scenario.persona ?? 'unknown';\n return { status: 200, json: { layoutOrder, assignments: scenario.variants ?? {}, persona, reasons: [] } };\n }\n\n if (route === '/v1/weights') {\n const components = Object.entries(scenario.weights ?? {}).map(([componentId, variants]) => ({ componentId, updatedAt: 0, variants }));\n return { status: 200, json: { components } };\n }\n\n return null;\n}\n","import { resolveScenario } from './resolve.js';\nimport { getSentientEvents, type CapturedEvent } from './events.js';\nimport type { SentientScenario } from './scenario.js';\n\ntype InitData = { overrides: Record<string, string>; layout: string[] | null };\n\ntype PwRoute = {\n request(): { method(): string; url(): string; postData(): string | null };\n fulfill(r: { status: number; contentType?: string; body?: string }): Promise<void>;\n continue(): Promise<void>;\n};\n\n/** Structural subset of Playwright's `Page` — avoids a hard dependency on @playwright/test. */\ntype PwPage = {\n addInitScript(script: (arg: InitData) => void, arg: InitData): Promise<void>;\n route(url: string, handler: (route: PwRoute) => unknown): Promise<void>;\n};\n\n/**\n * Make a Playwright `page` serve a SentientUI scenario: forces variants/layout\n * before load and stubs every `/v1/*` request from the scenario, capturing\n * posted events. Returns a handle exposing `.events()`.\n */\nexport async function mockSentient(\n page: PwPage,\n scenario: SentientScenario = {},\n): Promise<{ events: () => CapturedEvent[] }> {\n const initData: InitData = { overrides: scenario.variants ?? {}, layout: scenario.layout ?? null };\n await page.addInitScript((data: InitData) => {\n (window as unknown as { __sentient_overrides?: unknown }).__sentient_overrides = data.overrides;\n if (data.layout) {\n (window as unknown as { __sentient_layout_override?: unknown }).__sentient_layout_override = data.layout;\n }\n }, initData);\n\n await page.route('**/v1/**', async (route) => {\n const req = route.request();\n const resolved = await resolveScenario(scenario, req.method(), req.url(), req.postData());\n if (!resolved) return route.continue();\n await route.fulfill({\n status: resolved.status,\n contentType: 'application/json',\n body: JSON.stringify(resolved.json ?? {}),\n });\n });\n\n return { events: () => getSentientEvents() };\n}\n","import { resolveScenario } from './resolve.js';\nimport type { SentientScenario } from './scenario.js';\n\ntype CyReq = {\n method: string;\n url: string;\n body: unknown;\n reply(r: { statusCode: number; body?: unknown }): void;\n};\n\n/** Structural subset of Cypress's `cy` — avoids a hard dependency on cypress. */\ntype Cy = {\n intercept(url: string, handler: (req: CyReq) => void | Promise<void>): unknown;\n on(event: string, cb: (win: Record<string, unknown>) => void): unknown;\n};\n\n/**\n * Make Cypress serve a SentientUI scenario: forces variants/layout on the app\n * window before load and stubs every `/v1/*` request from the scenario. Call in\n * a `beforeEach` before `cy.visit`.\n *\n * Note: for event assertions in Cypress, alias the intercept (`cy.intercept(...).as('ev')`)\n * and `cy.wait('@ev')` — captured module state does not cross the browser/Node boundary.\n */\nexport function mockSentientCypress(cy: Cy, scenario: SentientScenario = {}): void {\n const overrides = scenario.variants ?? {};\n const layout = scenario.layout ?? null;\n\n cy.on('window:before:load', (win) => {\n win.__sentient_overrides = overrides;\n if (layout) win.__sentient_layout_override = layout;\n });\n\n cy.intercept('**/v1/**', async (req) => {\n const resolved = await resolveScenario(\n scenario,\n req.method,\n req.url,\n req.body != null ? JSON.stringify(req.body) : null,\n );\n if (!resolved) return; // passthrough\n req.reply({ statusCode: resolved.status, body: (resolved.json ?? '') as unknown });\n });\n}\n"],"mappings":"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","names":["render","createContext","useContext","useEffect","useMemo","useState","detectDeviceClass","detectTrafficSource","init","store","listeners","update","componentId","weights","store","set","listeners","cb","e","previewOn","listeners","getPreviewMode","previewOn","subscribePreview","fn","listeners","createPreviewClient","inner","componentId","segment","variantIds","agentData","agentDataByVariant","jsx","deriveDefaultSegment","_a","_b","device","detectDeviceClass","source","detectTrafficSource","e","AdaptiveContext","createContext","AdaptiveProvider","props","client","setClient","useState","sessionSegment","previewOn","setPreviewOn","getPreviewMode","useEffect","subscribePreview","prev","config","cancelled","created","initGraph","__spreadProps","__spreadValues","init","poll","entries","entry","weights","v","update","timerId","ssrFallback","exposedClient","useMemo","createPreviewClient","value","applyScenario","scenario","_a","w","__spreadValues","resetScenario","http","HttpResponse","captured","recordEvent","getSentientEvents","clearSentientEvents","hasFiredGoal","events","goalName","e","pathOf","url","_a","e","apiOverride","scenario","route","_b","_c","o","r","resolveScenario","_method","bodyText","_d","_e","_f","_g","_h","_i","_j","_k","_l","_m","_n","_o","path","override","body","recordEvent","b","s","layoutOrder","persona","componentId","variants","scenarioToHandlers","scenario","http","request","bodyText","r","resolveScenario","HttpResponse","mockSentient","page","scenario","_a","_b","initData","data","route","req","resolved","resolveScenario","getSentientEvents","mockSentientCypress","cy","scenario","_a","_b","overrides","layout","win","req","resolved","resolveScenario","jsx","renderWithSentient","ui","scenario","options","applyScenario","render","jsx","AdaptiveProvider","setupSentientTests","_a","g","resetScenario"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentientui/react",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -26,6 +26,16 @@
26
26
  "types": "./dist/devtools.d.ts",
27
27
  "import": "./dist/devtools.mjs",
28
28
  "require": "./dist/devtools.js"
29
+ },
30
+ "./testing": {
31
+ "types": "./dist/testing.d.ts",
32
+ "import": "./dist/testing.mjs",
33
+ "require": "./dist/testing.js"
34
+ },
35
+ "./testing/node": {
36
+ "types": "./dist/testing/node.d.ts",
37
+ "import": "./dist/testing/node.mjs",
38
+ "require": "./dist/testing/node.js"
29
39
  }
30
40
  },
31
41
  "files": [
@@ -35,18 +45,27 @@
35
45
  "@sentientui/core": "0.9.0"
36
46
  },
37
47
  "peerDependencies": {
38
- "react": ">=18.0.0",
39
- "next": ">=14.0.0"
48
+ "@testing-library/react": ">=14.0.0",
49
+ "msw": ">=2.0.0",
50
+ "next": ">=14.0.0",
51
+ "react": ">=18.0.0"
40
52
  },
41
53
  "peerDependenciesMeta": {
42
54
  "next": {
43
55
  "optional": true
56
+ },
57
+ "@testing-library/react": {
58
+ "optional": true
59
+ },
60
+ "msw": {
61
+ "optional": true
44
62
  }
45
63
  },
46
64
  "devDependencies": {
47
65
  "@testing-library/react": "^16.1.0",
48
66
  "@types/react": "^18.3.12",
49
67
  "jsdom": "^25.0.1",
68
+ "msw": "^2.15.0",
50
69
  "react": "^18.3.1",
51
70
  "react-dom": "^18.3.1",
52
71
  "tsup": "^8.3.5",