// @requirements REQ-PRODUCT-SCOPE-GUARDS // @requirements REQ-PRODUCT-PROCESSING-CHOICES // @requirements REQ-PRODUCT-REACT REQ-PRODUCT-CONSENT-RUNTIME REQ-PRODUCT-CONSENT-EVIDENCE REQ-PRODUCT-SAFE-CONTENT import {test as nodeTest} from 'node:test'; import assert from 'node:assert/strict'; import {createElement as h,StrictMode,act} from 'react'; import {renderToString,renderToStaticMarkup} from 'react-dom/server'; import {createRoot,hydrateRoot} from 'react-dom/client'; import {JSDOM} from 'jsdom'; import {createProcessingChoices,renderLegalDocument,renderLegalLinks} from 'notabenda'; import * as compatibilityReact from 'notabenda/react'; import * as compiledReact from 'notabenda/react/consent'; import {consentProfiles} from './consent-profiles.mjs'; import {LegalDocument,LegalLinks,CollectionNotice} from 'notabenda/react/documents'; import {compileLegalDocuments,createConsentNotice,createProcessingNotice,createCollectionNotice} from 'notabenda/content'; import {recordLegalRelease,registerConsentNotice,setActiveConsentNotice,recordConsentChoice,getConsentHead} from 'notabenda-cf'; import {createSyntheticStorage} from '../../notabenda-cf/examples/synthetic.mjs'; import {fixture} from './helpers.mjs'; import {App} from '../../../examples/react/view.mjs'; const instant='2026-10-18T12:00:00.000Z'; async function createSetup(createConsent,id='react-example') { const config=await fixture({reviewed:true});config.site.id=id; config.services=[{id:'optional-sample',name:'Fictional optional service',provider:'Fictional operator',location:'Local test',purposeIds:config.purposes.map(p=>p.id)}];config.storage=[]; config.consent={purposes:config.purposes.map(p=>({id:p.id,scope:'one'})),services:config.services.map(s=>({id:s.id,scope:'one'})),grantLifetimeDays:30,refusalLifetimeDays:180,dismissalMinutes:30}; const values=new Map();const storage={getItem:key=>values.get(key)??null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}; const env={storage,now:()=>Date.parse(instant),schedule:()=>()=>{}}; return {config,values,env,purpose:config.consent.purposes[0].id,controller:createConsent(config,env)}; } async function domTest(callback,html='') { const dom=new JSDOM(`
${html}
`,{url:'https://fictional.example.test/'}); const keys=['window','document','HTMLElement','Node','Event','MouseEvent','IS_REACT_ACT_ENVIRONMENT'];const previous=new Map(keys.map(key=>[key,Object.getOwnPropertyDescriptor(globalThis,key)])); for(const key of keys)Object.defineProperty(globalThis,key,{value:key==='IS_REACT_ACT_ENVIRONMENT'?true:dom.window[key],configurable:true,writable:true}); // jsdom has no native modal implementation. Real dialog/layout behavior is verified separately in Chromium. dom.window.HTMLDialogElement.prototype.showModal=function(){this.open=true;}; dom.window.HTMLDialogElement.prototype.close=function(){this.open=false;this.dispatchEvent(new dom.window.Event('close'));}; try{await callback(dom.window.document.getElementById('root'),dom.window);}finally{dom.window.close();for(const [key,value] of previous)value?Object.defineProperty(globalThis,key,value):delete globalThis[key];} } for(const [index,[profile,createConsent]] of consentProfiles.entries()){ const test=(name,run)=>nodeTest(`${profile}: ${name}`,run); const setup=id=>createSetup(createConsent,id); const {ConsentProvider,ConsentControls,PreferencesButton,useConsentController,useConsentSnapshot,usePurposeAllowed,useServiceAllowed}=index===0?compatibilityReact:compiledReact; const Probe=({purpose,onSnapshot})=>{const state=useConsentSnapshot();const allowed=usePurposeAllowed(purpose);onSnapshot?.(state);return h('output',{'data-permission':allowed?'allowed':'blocked'},state.purposes[purpose]??'unknown');}; // @requirements REQ-PRODUCT-PRIVACY-SIGNALS test('React service permission remains distinct from cookie permission, with pure immutable signal snapshots and conservative SSR',async()=>{ const f=await setup();f.controller.destroy();f.config.services[0].processingActivities=['sharing'];f.config.privacySignals={globalPrivacyControl:{appliesTo:['sharing'],explanation:'Fictional sharing restriction.'}}; const controller=createConsent(f.config,{...f.env,globalPrivacyControl:()=>true});controller.acceptAll(); let captured;const SignalProbe=()=>{captured=useConsentSnapshot();return h('output',{'data-cookie':usePurposeAllowed(f.purpose),'data-service':useServiceAllowed('optional-sample')},captured.privacySignals?.globalPrivacyControl??'unknown');}; const tree=h(ConsentProvider,{controller},h(SignalProbe));const html=renderToString(tree);assert.match(html,/data-cookie="false" data-service="false"/); controller.isServiceAllowed=()=>{throw new Error('React render cannot invoke effectful service checks.');}; await domTest(async container=>{const root=createRoot(container);await act(async()=>root.render(tree));assert.equal(container.querySelector('output').dataset.cookie,'true');assert.equal(container.querySelector('output').dataset.service,'false');assert.equal(captured.privacySignals.globalPrivacyControl,'observed');assert.throws(()=>captured.servicePermissions['optional-sample']=true,TypeError);assert.throws(()=>captured.privacySignals.restrictedServices['optional-sample'].push('sale'),TypeError);await act(async()=>root.unmount());});controller.destroy(); }); test('React processing state is deeply immutable, conservative during reads and never invokes effectful permission checks',async()=>{ const f=await setup();f.controller.destroy();f.config.services[0].processingActivities=['sale']; const release=compileLegalDocuments(f.config),notice=createProcessingNotice(release.artifact,{scopeId:'privacy-choices',activities:['sale'],wording:{title:'Privacy choices',description:'Fictional opt-out.',actionLabel:'Opt out'}}); const processing=createProcessingChoices(notice,{streamRef:'a'.repeat(64),read:()=>new Promise(()=>{}),submit:async()=>{throw new Error('Unused');}},{storage:f.env.storage}); const controller=createConsent(f.config,f.env,{processingChoices:processing});controller.acceptAll();let captured; controller.isServiceAllowed=()=>{throw new Error('Effectful check during render.');}; const Component=()=>{captured=useConsentSnapshot();return h('output',{'data-cookie':usePurposeAllowed(f.purpose),'data-service':useServiceAllowed('optional-sample')},captured.processingChoices?.status);}; const tree=h(ConsentProvider,{controller},h(Component));assert.match(renderToString(tree),/data-cookie="false" data-service="false"/); await domTest(async container=>{const root=createRoot(container);await act(async()=>root.render(tree));assert.equal(container.querySelector('output').dataset.cookie,'true');assert.equal(container.querySelector('output').dataset.service,'false');assert.equal(captured.processingChoices.status,'checking');assert.throws(()=>captured.processingChoices.blockedActivities.pop(),TypeError);assert.throws(()=>captured.processingChoices.restrictedActivities.push('sale'),TypeError);assert.throws(()=>captured.processingChoices.revision=1,TypeError);await act(async()=>root.unmount());}); controller.destroy();processing.destroy();await processing.settled(); }); test('React document components preserve exact core escaping and work without a provider or client hooks',async()=>{ const f=await setup();f.config.documents.terms.sections[0].paragraphs=['Literal text.']; assert.equal(renderToStaticMarkup(h(LegalDocument,{config:f.config,kind:'terms'})),`
${renderLegalDocument(f.config,'terms')}
`); assert.equal(renderToStaticMarkup(h(LegalLinks,{config:f.config})),`
${renderLegalLinks(f.config)}
`); assert.doesNotMatch(renderToStaticMarkup(h(LegalDocument,{config:f.config,kind:'terms'})),/